|
76352
|
NULL
|
0
|
2026-04-24T07:43:43.475854+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777016623475_m1.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"2 files committed","depth":2,"role_description":"text"},{"role":"AXTextField","text":"JY-20157 add test coverage","depth":3,"value":"JY-20157 add test coverage","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-9028934553602785414
|
8404466858115443482
|
app_switch
|
accessibility
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}...
|
NULL
|
|
76353
|
NULL
|
0
|
2026-04-24T07:43:43.627756+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777016623627_m2.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"2 files committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"role_description":"text"},{"role":"AXTextField","text":"JY-20157 add test coverage","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"value":"JY-20157 add test coverage","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.05817819,"height":0.013567438},"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.12134308,"height":0.025538707},"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.796875,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"bounds":{"left":0.8121675,"top":0.019952115,"width":0.103390954,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.59541225,"top":0.32322428,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6050532,"top":0.3216281,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.61236703,"top":0.3216281,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"bounds":{"left":0.3799867,"top":0.070231445,"width":0.34375,"height":0.92976856},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.30851063,"top":0.05027933,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.31948137,"top":0.05027933,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.328125,"top":0.05027933,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.33676863,"top":0.05027933,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.34541222,"top":0.05027933,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
105150503854657824
|
8404466858115443482
|
app_switch
|
accessibility
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
76370
|
1912
|
8
|
2026-04-24T07:46:32.941438+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777016792941_m2.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"2 files committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"role_description":"text"},{"role":"AXTextField","text":"JY-20157 add test coverage","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"value":"JY-20157 add test coverage","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.05817819,"height":0.013567438},"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.12134308,"height":0.025538707},"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.796875,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"bounds":{"left":0.8121675,"top":0.019952115,"width":0.103390954,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.59541225,"top":0.32322428,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6050532,"top":0.3216281,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.61236703,"top":0.3216281,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"bounds":{"left":0.3799867,"top":0.070231445,"width":0.34375,"height":0.92976856},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
105150503854657824
|
8404466858115443482
|
app_switch
|
accessibility
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
76371
|
1911
|
8
|
2026-04-24T07:46:33.003528+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777016793003_m1.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"2 files committed","depth":2,"role_description":"text"},{"role":"AXTextField","text":"JY-20157 add test coverage","depth":3,"value":"JY-20157 add test coverage","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
105150503854657824
|
8404466858115443482
|
app_switch
|
accessibility
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
76372
|
1911
|
9
|
2026-04-24T07:47:03.307638+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777016823307_m1.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"2 files committed","depth":2,"role_description":"text"},{"role":"AXTextField","text":"JY-20157 add test coverage","depth":3,"value":"JY-20157 add test coverage","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
105150503854657824
|
8404466858115443482
|
idle
|
accessibility
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
76371
|
|
76373
|
1912
|
9
|
2026-04-24T07:47:03.307640+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777016823307_m2.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"2 files committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"role_description":"text"},{"role":"AXTextField","text":"JY-20157 add test coverage","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"value":"JY-20157 add test coverage","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.05817819,"height":0.013567438},"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.12134308,"height":0.025538707},"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.796875,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"bounds":{"left":0.8121675,"top":0.019952115,"width":0.103390954,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.59541225,"top":0.32322428,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6050532,"top":0.3216281,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.61236703,"top":0.3216281,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"bounds":{"left":0.3799867,"top":0.070231445,"width":0.34375,"height":0.92976856},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
105150503854657824
|
8404466858115443482
|
idle
|
accessibility
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
76370
|
|
76374
|
1911
|
10
|
2026-04-24T07:47:33.708510+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777016853708_m1.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"2 files committed","depth":2,"role_description":"text"},{"role":"AXTextField","text":"JY-20157 add test coverage","depth":3,"value":"JY-20157 add test coverage","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
105150503854657824
|
8404466858115443482
|
idle
|
accessibility
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
76371
|
|
76375
|
1912
|
10
|
2026-04-24T07:47:33.811903+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777016853811_m2.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"2 files committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"role_description":"text"},{"role":"AXTextField","text":"JY-20157 add test coverage","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"value":"JY-20157 add test coverage","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.05817819,"height":0.013567438},"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.12134308,"height":0.025538707},"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.796875,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"bounds":{"left":0.8121675,"top":0.019952115,"width":0.103390954,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.59541225,"top":0.32322428,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6050532,"top":0.3216281,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.61236703,"top":0.3216281,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"bounds":{"left":0.3799867,"top":0.070231445,"width":0.34375,"height":0.92976856},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
105150503854657824
|
8404466858115443482
|
idle
|
accessibility
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
76370
|
|
76390
|
1914
|
3
|
2026-04-24T07:50:16.319062+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777017016319_m2.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"2 files committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"role_description":"text"},{"role":"AXTextField","text":"JY-20157 add test coverage","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"value":"JY-20157 add test coverage","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.05817819,"height":0.013567438},"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.12134308,"height":0.025538707},"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.796875,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"bounds":{"left":0.8121675,"top":0.019952115,"width":0.103390954,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.59541225,"top":0.32322428,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6050532,"top":0.3216281,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.61236703,"top":0.3216281,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"bounds":{"left":0.3799867,"top":0.070231445,"width":0.34375,"height":0.92976856},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7089281698504681203
|
8404466858115443482
|
app_switch
|
accessibility
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…...
|
NULL
|
|
76391
|
1913
|
3
|
2026-04-24T07:50:16.385007+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777017016385_m1.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"2 files committed","depth":2,"role_description":"text"},{"role":"AXTextField","text":"JY-20157 add test coverage","depth":3,"value":"JY-20157 add test coverage","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
105150503854657824
|
8404466858115443482
|
app_switch
|
accessibility
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
76464
|
1917
|
6
|
2026-04-24T08:00:07.788993+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777017607788_m1.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"2 files committed","depth":2,"role_description":"text"},{"role":"AXTextField","text":"JY-20157 add test coverage","depth":3,"value":"JY-20157 add test coverage","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
105150503854657824
|
8404466858115443482
|
app_switch
|
accessibility
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
76465
|
1918
|
6
|
2026-04-24T08:00:07.898932+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777017607898_m2.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"2 files committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"role_description":"text"},{"role":"AXTextField","text":"JY-20157 add test coverage","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"value":"JY-20157 add test coverage","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.05817819,"height":0.013567438},"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.12134308,"height":0.025538707},"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.796875,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"bounds":{"left":0.8121675,"top":0.019952115,"width":0.103390954,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.59541225,"top":0.32322428,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6050532,"top":0.3216281,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.61236703,"top":0.3216281,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"bounds":{"left":0.3799867,"top":0.070231445,"width":0.34375,"height":0.92976856},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.30851063,"top":0.05027933,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.31948137,"top":0.05027933,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.328125,"top":0.05027933,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.33676863,"top":0.05027933,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.34541222,"top":0.05027933,"width":0.008643617,"height":0.01915403},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
105150503854657824
|
8404466858115443482
|
app_switch
|
accessibility
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
76471
|
1918
|
9
|
2026-04-24T08:00:13.656135+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777017613656_m2.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"2 files committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"role_description":"text"},{"role":"AXTextField","text":"JY-20157 add test coverage","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"value":"JY-20157 add test coverage","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.05817819,"height":0.013567438},"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.12134308,"height":0.025538707},"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.796875,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"bounds":{"left":0.8121675,"top":0.019952115,"width":0.103390954,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.59541225,"top":0.32322428,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6050532,"top":0.3216281,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.61236703,"top":0.3216281,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"bounds":{"left":0.3799867,"top":0.070231445,"width":0.34375,"height":0.92976856},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
105150503854657824
|
8404466858115443482
|
click
|
accessibility
|
NULL
|
2 files committed
JY-20157 add test coverage
text/ 2 files committed
JY-20157 add test coverage
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
76469
|
|
76777
|
1926
|
15
|
2026-04-24T08:24:46.691443+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777019086691_m2.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20738-debug-AJ-tracking-UP, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.08510638,"height":0.025538707},"help_text":"Git Branch: JY-20738-debug-AJ-tracking-UP","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.63796544,"top":0.12529927,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6476064,"top":0.123703115,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.6549202,"top":0.123703115,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7284345398556861878
|
8404466858115443482
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
76775
|
|
76786
|
1925
|
24
|
2026-04-24T08:24:57.445732+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777019097445_m1.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php, class
HistoryService.php, class
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch, folder
Eloquent, folder
Encoding, folder
Encryption, folder
ES, folder
Faker, folder
FeatureFlags, folder
FFMpeg, folder
FileSystem, folder
Gecko, folder
Gong, folder
GuzzleHttp, folder
KeyPoints, folder
Kiosk, folder
LanguageDetection, folder
LiveFeed, folder
Locks, folder
Math, folder
MediaPipeline, folder
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder
ParagraphBreaker, folder
ParticipantSpeech, folder
PartitionedCookie, folder
PlaybackPage, folder
Playlist, folder
Prophet, folder
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder
Slack, folder
TeamInsights, folder
TimeMemoryMapper, folder
Transcription, folder
TranscriptionSummary, folder
Twilio, folder
Uploader, folder
UrlGenerator, folder
Utility, folder
Uuid, folder
Waveform, folder
Webhooks, folder
Workflow, folder
Configuration, folder
Console, folder
Commands, folder
Activities, folder
Analytics, folder
Calendars, folder
Crm, folder
Hubspot, folder
IntegrationApp, folder
Traits, folder
AddLayoutEntities.php, class
AutologDelayedCommand.php, class...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20738-debug-AJ-tracking-UP, menu","depth":5,"help_text":"Git Branch: JY-20738-debug-AJ-tracking-UP","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Country, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Database, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Datadog, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Encoding, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Encryption, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ES, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Faker, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Gecko, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Gong, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Locks, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Math, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"MeetingBot, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"MobileSettings, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Model, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Notification, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Nudge, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ParagraphBreaker, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ParticipantSpeech, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"PartitionedCookie, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackPage, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Playlist, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Prophet, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ProsperWorks, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Queue, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Router, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Saml2, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"SCIM, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Seeder, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Sentry, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Serializer, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Settings, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Sidekick, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"TimeMemoryMapper, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"TranscriptionSummary, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Uploader, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"UrlGenerator, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Utility, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Uuid, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Waveform, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Webhooks, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Workflow, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Configuration, folder","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Console, folder","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Commands, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Activities, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Analytics, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Calendars, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Crm, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Hubspot, folder","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"IntegrationApp, folder","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"Traits, folder","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"AddLayoutEntities.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"AutologDelayedCommand.php, class","depth":11,"role_description":"text"}]...
|
105049981535002002
|
8404466858115312522
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php, class
HistoryService.php, class
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch, folder
Eloquent, folder
Encoding, folder
Encryption, folder
ES, folder
Faker, folder
FeatureFlags, folder
FFMpeg, folder
FileSystem, folder
Gecko, folder
Gong, folder
GuzzleHttp, folder
KeyPoints, folder
Kiosk, folder
LanguageDetection, folder
LiveFeed, folder
Locks, folder
Math, folder
MediaPipeline, folder
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder
ParagraphBreaker, folder
ParticipantSpeech, folder
PartitionedCookie, folder
PlaybackPage, folder
Playlist, folder
Prophet, folder
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder
Slack, folder
TeamInsights, folder
TimeMemoryMapper, folder
Transcription, folder
TranscriptionSummary, folder
Twilio, folder
Uploader, folder
UrlGenerator, folder
Utility, folder
Uuid, folder
Waveform, folder
Webhooks, folder
Workflow, folder
Configuration, folder
Console, folder
Commands, folder
Activities, folder
Analytics, folder
Calendars, folder
Crm, folder
Hubspot, folder
IntegrationApp, folder
Traits, folder
AddLayoutEntities.php, class
AutologDelayedCommand.php, class...
|
NULL
|
|
76779
|
1926
|
16
|
2026-04-24T08:24:48.001789+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777019088001_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php, class
HistoryService.php, class
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch, folder
Eloquent, folder
Encoding, folder
Encryption, folder
ES, folder
Faker, folder
FeatureFlags, folder
FFMpeg, folder
FileSystem, folder
Gecko, folder
Gong, folder
GuzzleHttp, folder
KeyPoints, folder
Kiosk, folder
LanguageDetection, folder
LiveFeed, folder
Locks, folder
Math, folder
MediaPipeline, folder
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20738-debug-AJ-tracking-UP, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.08510638,"height":0.025538707},"help_text":"Git Branch: JY-20738-debug-AJ-tracking-UP","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.63796544,"top":0.12529927,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6476064,"top":0.123703115,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.6549202,"top":0.123703115,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"bounds":{"left":0.3799867,"top":0.0,"width":0.34375,"height":1.0},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Country, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Database, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Datadog, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Encoding, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Encryption, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ES, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Faker, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Gecko, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Gong, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Locks, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Math, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"MeetingBot, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"MobileSettings, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Model, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Notification, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Nudge, folder","depth":9,"role_description":"text"}]...
|
-3363048790979053516
|
8404466858115312410
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php, class
HistoryService.php, class
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch, folder
Eloquent, folder
Encoding, folder
Encryption, folder
ES, folder
Faker, folder
FeatureFlags, folder
FFMpeg, folder
FileSystem, folder
Gecko, folder
Gong, folder
GuzzleHttp, folder
KeyPoints, folder
Kiosk, folder
LanguageDetection, folder
LiveFeed, folder
Locks, folder
Math, folder
MediaPipeline, folder
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder...
|
NULL
|
|
75757
|
1890
|
1
|
2026-04-24T06:47:33.279732+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777013253279_m2.jpg...
|
PhpStorm
|
faVsco.js – TrackAutomatedReportGeneratedEventTest faVsco.js – TrackAutomatedReportGeneratedEventTest.php...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12011 on JY-20157-AJ-rep Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
549
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.12134308,"height":0.025538707},"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.796875,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"bounds":{"left":0.8121675,"top":0.019952115,"width":0.103390954,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.59574467,"top":0.32322428,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.60538566,"top":0.3216281,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.61269945,"top":0.3216281,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"bounds":{"left":0.3799867,"top":0.3200319,"width":0.34375,"height":0.6799681},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"549","depth":4,"bounds":{"left":0.95977396,"top":0.10055866,"width":0.012632979,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7423652625051804765
|
8404466789400030104
|
app_switch
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12011 on JY-20157-AJ-rep Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
549
Previous Highlighted Error
Next Highlighted Error...
|
NULL
|
|
76669
|
1923
|
7
|
2026-04-24T08:15:48.171211+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777018548171_m1.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
try {
foreach ($this->resolveUsers($automatedReport) as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20738-debug-AJ-tracking-UP, menu","depth":5,"help_text":"Git Branch: JY-20738-debug-AJ-tracking-UP","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n try {\n foreach ($this->resolveUsers($automatedReport) as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n try {\n foreach ($this->resolveUsers($automatedReport) as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-22530481736454815
|
8404464659031862019
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
try {
foreach ($this->resolveUsers($automatedReport) as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
76670
|
1924
|
5
|
2026-04-24T08:15:48.171291+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777018548171_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
try {
foreach ($this->resolveUsers($automatedReport) as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20738-debug-AJ-tracking-UP, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.08510638,"height":0.025538707},"help_text":"Git Branch: JY-20738-debug-AJ-tracking-UP","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6193484,"top":0.2490024,"width":0.00731383,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.6286569,"top":0.2490024,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.63863033,"top":0.2490024,"width":0.00731383,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6476064,"top":0.24740623,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.6549202,"top":0.24740623,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n try {\n foreach ($this->resolveUsers($automatedReport) as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}","depth":4,"bounds":{"left":0.3773271,"top":0.105347164,"width":0.28457448,"height":0.89465284},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n try {\n foreach ($this->resolveUsers($automatedReport) as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-22530481736454815
|
8404464659031862019
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
try {
foreach ($this->resolveUsers($automatedReport) as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
75771
|
1889
|
8
|
2026-04-24T06:48:40.277071+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777013320277_m1.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12011 on JY-20157-AJ-rep Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
20
Previous Highlighted Error
Next Highlighted Error
[2026-04-24 06:48:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"56544ba5-de8a-4826-9181-c1f98b641c2d","trace_id":"0d23b863-4f54-4038-a81e-1e6b14b68993"}
[2026-04-24 06:48:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"56544ba5-de8a-4826-9181-c1f98b641c2d","trace_id":"0d23b863-4f54-4038-a81e-1e6b14b68993"}
[2026-04-24 06:48:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"56544ba5-de8a-4826-9181-c1f98b641c2d","trace_id":"0d23b863-4f54-4038-a81e-1e6b14b68993"}
[2026-04-24 06:48:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"57b8f721-5f9c-4555-b09f-7b30b6bb53bd","trace_id":"ac32f526-cd51-4a22-9072-6555fd1d93ce"}
[2026-04-24 06:48:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"57b8f721-5f9c-4555-b09f-7b30b6bb53bd","trace_id":"ac32f526-cd51-4a22-9072-6555fd1d93ce"}
[2026-04-24 06:48:07] local.NOTICE: Monitoring start {"correlation_id":"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516","trace_id":"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee"}
[2026-04-24 06:48:07] local.NOTICE: Monitoring end {"correlation_id":"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516","trace_id":"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee"}
[2026-04-24 06:48:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"84127380-7279-44d6-862c-15b2b5cba112","trace_id":"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707"}
[2026-04-24 06:48:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"84127380-7279-44d6-862c-15b2b5cba112","trace_id":"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707"}
[2026-04-24 06:48:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:11] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:11] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:12] local.INFO: Running conference:monitor:count command for activities in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:12] local.INFO: [conference:monitor:count] No activities found in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"f7242472-e47d-4264-b55d-829ff8be2c5b","trace_id":"5eebd870-6b02-484c-b745-10409d1fe0c5"}
[2026-04-24 06:48:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"f7242472-e47d-4264-b55d-829ff8be2c5b","trace_id":"5eebd870-6b02-484c-b745-10409d1fe0c5"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"20","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-04-24 06:48:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"56544ba5-de8a-4826-9181-c1f98b641c2d\",\"trace_id\":\"0d23b863-4f54-4038-a81e-1e6b14b68993\"}\n[2026-04-24 06:48:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"56544ba5-de8a-4826-9181-c1f98b641c2d\",\"trace_id\":\"0d23b863-4f54-4038-a81e-1e6b14b68993\"}\n[2026-04-24 06:48:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"56544ba5-de8a-4826-9181-c1f98b641c2d\",\"trace_id\":\"0d23b863-4f54-4038-a81e-1e6b14b68993\"}\n[2026-04-24 06:48:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"57b8f721-5f9c-4555-b09f-7b30b6bb53bd\",\"trace_id\":\"ac32f526-cd51-4a22-9072-6555fd1d93ce\"}\n[2026-04-24 06:48:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"57b8f721-5f9c-4555-b09f-7b30b6bb53bd\",\"trace_id\":\"ac32f526-cd51-4a22-9072-6555fd1d93ce\"}\n[2026-04-24 06:48:07] local.NOTICE: Monitoring start {\"correlation_id\":\"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516\",\"trace_id\":\"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee\"}\n[2026-04-24 06:48:07] local.NOTICE: Monitoring end {\"correlation_id\":\"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516\",\"trace_id\":\"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee\"}\n[2026-04-24 06:48:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"84127380-7279-44d6-862c-15b2b5cba112\",\"trace_id\":\"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707\"}\n[2026-04-24 06:48:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"84127380-7279-44d6-862c-15b2b5cba112\",\"trace_id\":\"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707\"}\n[2026-04-24 06:48:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:11] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:11] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:12] local.INFO: Running conference:monitor:count command for activities in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:12] local.INFO: [conference:monitor:count] No activities found in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"f7242472-e47d-4264-b55d-829ff8be2c5b\",\"trace_id\":\"5eebd870-6b02-484c-b745-10409d1fe0c5\"}\n[2026-04-24 06:48:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"f7242472-e47d-4264-b55d-829ff8be2c5b\",\"trace_id\":\"5eebd870-6b02-484c-b745-10409d1fe0c5\"}","depth":4,"value":"[2026-04-24 06:48:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"56544ba5-de8a-4826-9181-c1f98b641c2d\",\"trace_id\":\"0d23b863-4f54-4038-a81e-1e6b14b68993\"}\n[2026-04-24 06:48:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"56544ba5-de8a-4826-9181-c1f98b641c2d\",\"trace_id\":\"0d23b863-4f54-4038-a81e-1e6b14b68993\"}\n[2026-04-24 06:48:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"56544ba5-de8a-4826-9181-c1f98b641c2d\",\"trace_id\":\"0d23b863-4f54-4038-a81e-1e6b14b68993\"}\n[2026-04-24 06:48:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"57b8f721-5f9c-4555-b09f-7b30b6bb53bd\",\"trace_id\":\"ac32f526-cd51-4a22-9072-6555fd1d93ce\"}\n[2026-04-24 06:48:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"57b8f721-5f9c-4555-b09f-7b30b6bb53bd\",\"trace_id\":\"ac32f526-cd51-4a22-9072-6555fd1d93ce\"}\n[2026-04-24 06:48:07] local.NOTICE: Monitoring start {\"correlation_id\":\"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516\",\"trace_id\":\"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee\"}\n[2026-04-24 06:48:07] local.NOTICE: Monitoring end {\"correlation_id\":\"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516\",\"trace_id\":\"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee\"}\n[2026-04-24 06:48:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"84127380-7279-44d6-862c-15b2b5cba112\",\"trace_id\":\"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707\"}\n[2026-04-24 06:48:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"84127380-7279-44d6-862c-15b2b5cba112\",\"trace_id\":\"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707\"}\n[2026-04-24 06:48:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:11] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:11] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:12] local.INFO: Running conference:monitor:count command for activities in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:12] local.INFO: [conference:monitor:count] No activities found in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"f7242472-e47d-4264-b55d-829ff8be2c5b\",\"trace_id\":\"5eebd870-6b02-484c-b745-10409d1fe0c5\"}\n[2026-04-24 06:48:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"f7242472-e47d-4264-b55d-829ff8be2c5b\",\"trace_id\":\"5eebd870-6b02-484c-b745-10409d1fe0c5\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7029277336993072845
|
8404260221874187198
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12011 on JY-20157-AJ-rep Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
20
Previous Highlighted Error
Next Highlighted Error
[2026-04-24 06:48:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"56544ba5-de8a-4826-9181-c1f98b641c2d","trace_id":"0d23b863-4f54-4038-a81e-1e6b14b68993"}
[2026-04-24 06:48:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"56544ba5-de8a-4826-9181-c1f98b641c2d","trace_id":"0d23b863-4f54-4038-a81e-1e6b14b68993"}
[2026-04-24 06:48:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"56544ba5-de8a-4826-9181-c1f98b641c2d","trace_id":"0d23b863-4f54-4038-a81e-1e6b14b68993"}
[2026-04-24 06:48:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"57b8f721-5f9c-4555-b09f-7b30b6bb53bd","trace_id":"ac32f526-cd51-4a22-9072-6555fd1d93ce"}
[2026-04-24 06:48:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"57b8f721-5f9c-4555-b09f-7b30b6bb53bd","trace_id":"ac32f526-cd51-4a22-9072-6555fd1d93ce"}
[2026-04-24 06:48:07] local.NOTICE: Monitoring start {"correlation_id":"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516","trace_id":"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee"}
[2026-04-24 06:48:07] local.NOTICE: Monitoring end {"correlation_id":"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516","trace_id":"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee"}
[2026-04-24 06:48:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"84127380-7279-44d6-862c-15b2b5cba112","trace_id":"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707"}
[2026-04-24 06:48:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"84127380-7279-44d6-862c-15b2b5cba112","trace_id":"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707"}
[2026-04-24 06:48:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:11] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:11] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:12] local.INFO: Running conference:monitor:count command for activities in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:12] local.INFO: [conference:monitor:count] No activities found in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"f7242472-e47d-4264-b55d-829ff8be2c5b","trace_id":"5eebd870-6b02-484c-b745-10409d1fe0c5"}
[2026-04-24 06:48:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"f7242472-e47d-4264-b55d-829ff8be2c5b","trace_id":"5eebd870-6b02-484c-b745-10409d1fe0c5"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
75772
|
1890
|
8
|
2026-04-24T06:48:40.277069+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777013320277_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12011 on JY-20157-AJ-rep Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
20
Previous Highlighted Error
Next Highlighted Error
[2026-04-24 06:48:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"56544ba5-de8a-4826-9181-c1f98b641c2d","trace_id":"0d23b863-4f54-4038-a81e-1e6b14b68993"}
[2026-04-24 06:48:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"56544ba5-de8a-4826-9181-c1f98b641c2d","trace_id":"0d23b863-4f54-4038-a81e-1e6b14b68993"}
[2026-04-24 06:48:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"56544ba5-de8a-4826-9181-c1f98b641c2d","trace_id":"0d23b863-4f54-4038-a81e-1e6b14b68993"}
[2026-04-24 06:48:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"57b8f721-5f9c-4555-b09f-7b30b6bb53bd","trace_id":"ac32f526-cd51-4a22-9072-6555fd1d93ce"}
[2026-04-24 06:48:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"57b8f721-5f9c-4555-b09f-7b30b6bb53bd","trace_id":"ac32f526-cd51-4a22-9072-6555fd1d93ce"}
[2026-04-24 06:48:07] local.NOTICE: Monitoring start {"correlation_id":"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516","trace_id":"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee"}
[2026-04-24 06:48:07] local.NOTICE: Monitoring end {"correlation_id":"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516","trace_id":"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee"}
[2026-04-24 06:48:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"84127380-7279-44d6-862c-15b2b5cba112","trace_id":"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707"}
[2026-04-24 06:48:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"84127380-7279-44d6-862c-15b2b5cba112","trace_id":"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707"}
[2026-04-24 06:48:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:11] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:11] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:12] local.INFO: Running conference:monitor:count command for activities in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:12] local.INFO: [conference:monitor:count] No activities found in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"f7242472-e47d-4264-b55d-829ff8be2c5b","trace_id":"5eebd870-6b02-484c-b745-10409d1fe0c5"}
[2026-04-24 06:48:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"f7242472-e47d-4264-b55d-829ff8be2c5b","trace_id":"5eebd870-6b02-484c-b745-10409d1fe0c5"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12011 on JY-20157-AJ-report-not-send-notification, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.12134308,"height":0.025538707},"help_text":"Pull request #12011 exists for current branch JY-20157-AJ-report-not-send-notification","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.796875,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"bounds":{"left":0.8121675,"top":0.019952115,"width":0.103390954,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.59574467,"top":0.32322428,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.60538566,"top":0.3216281,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.61269945,"top":0.3216281,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"bounds":{"left":0.3799867,"top":0.3200319,"width":0.34375,"height":0.6799681},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"20","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-04-24 06:48:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"56544ba5-de8a-4826-9181-c1f98b641c2d\",\"trace_id\":\"0d23b863-4f54-4038-a81e-1e6b14b68993\"}\n[2026-04-24 06:48:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"56544ba5-de8a-4826-9181-c1f98b641c2d\",\"trace_id\":\"0d23b863-4f54-4038-a81e-1e6b14b68993\"}\n[2026-04-24 06:48:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"56544ba5-de8a-4826-9181-c1f98b641c2d\",\"trace_id\":\"0d23b863-4f54-4038-a81e-1e6b14b68993\"}\n[2026-04-24 06:48:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"57b8f721-5f9c-4555-b09f-7b30b6bb53bd\",\"trace_id\":\"ac32f526-cd51-4a22-9072-6555fd1d93ce\"}\n[2026-04-24 06:48:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"57b8f721-5f9c-4555-b09f-7b30b6bb53bd\",\"trace_id\":\"ac32f526-cd51-4a22-9072-6555fd1d93ce\"}\n[2026-04-24 06:48:07] local.NOTICE: Monitoring start {\"correlation_id\":\"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516\",\"trace_id\":\"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee\"}\n[2026-04-24 06:48:07] local.NOTICE: Monitoring end {\"correlation_id\":\"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516\",\"trace_id\":\"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee\"}\n[2026-04-24 06:48:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"84127380-7279-44d6-862c-15b2b5cba112\",\"trace_id\":\"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707\"}\n[2026-04-24 06:48:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"84127380-7279-44d6-862c-15b2b5cba112\",\"trace_id\":\"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707\"}\n[2026-04-24 06:48:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:11] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:11] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:12] local.INFO: Running conference:monitor:count command for activities in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:12] local.INFO: [conference:monitor:count] No activities found in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"f7242472-e47d-4264-b55d-829ff8be2c5b\",\"trace_id\":\"5eebd870-6b02-484c-b745-10409d1fe0c5\"}\n[2026-04-24 06:48:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"f7242472-e47d-4264-b55d-829ff8be2c5b\",\"trace_id\":\"5eebd870-6b02-484c-b745-10409d1fe0c5\"}","depth":4,"bounds":{"left":0.63863033,"top":0.09736632,"width":0.36136967,"height":0.8818835},"value":"[2026-04-24 06:48:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"56544ba5-de8a-4826-9181-c1f98b641c2d\",\"trace_id\":\"0d23b863-4f54-4038-a81e-1e6b14b68993\"}\n[2026-04-24 06:48:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"56544ba5-de8a-4826-9181-c1f98b641c2d\",\"trace_id\":\"0d23b863-4f54-4038-a81e-1e6b14b68993\"}\n[2026-04-24 06:48:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"56544ba5-de8a-4826-9181-c1f98b641c2d\",\"trace_id\":\"0d23b863-4f54-4038-a81e-1e6b14b68993\"}\n[2026-04-24 06:48:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"57b8f721-5f9c-4555-b09f-7b30b6bb53bd\",\"trace_id\":\"ac32f526-cd51-4a22-9072-6555fd1d93ce\"}\n[2026-04-24 06:48:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"57b8f721-5f9c-4555-b09f-7b30b6bb53bd\",\"trace_id\":\"ac32f526-cd51-4a22-9072-6555fd1d93ce\"}\n[2026-04-24 06:48:07] local.NOTICE: Monitoring start {\"correlation_id\":\"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516\",\"trace_id\":\"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee\"}\n[2026-04-24 06:48:07] local.NOTICE: Monitoring end {\"correlation_id\":\"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516\",\"trace_id\":\"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee\"}\n[2026-04-24 06:48:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"84127380-7279-44d6-862c-15b2b5cba112\",\"trace_id\":\"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707\"}\n[2026-04-24 06:48:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"84127380-7279-44d6-862c-15b2b5cba112\",\"trace_id\":\"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707\"}\n[2026-04-24 06:48:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:11] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:11] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"48ee9d4b-28ae-434f-98c1-370869a770f3\",\"trace_id\":\"24b268f4-e271-446f-9128-d2c93e688267\"}\n[2026-04-24 06:48:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:12] local.INFO: Running conference:monitor:count command for activities in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:12] local.INFO: [conference:monitor:count] No activities found in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"9f6b9c79-fd3b-49e7-9569-92581a34675d\",\"trace_id\":\"c29e3d24-0809-4cec-a901-51862bce4478\"}\n[2026-04-24 06:48:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.723} {\"correlation_id\":\"f7242472-e47d-4264-b55d-829ff8be2c5b\",\"trace_id\":\"5eebd870-6b02-484c-b745-10409d1fe0c5\"}\n[2026-04-24 06:48:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.723,\"memoryPeakAfterCommandInMB\":99.723} {\"correlation_id\":\"f7242472-e47d-4264-b55d-829ff8be2c5b\",\"trace_id\":\"5eebd870-6b02-484c-b745-10409d1fe0c5\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7029277336993072845
|
8404260221874187198
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12011 on JY-20157-AJ-rep Project: faVsco.js, menu
#12011 on JY-20157-AJ-report-not-send-notification, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
20
Previous Highlighted Error
Next Highlighted Error
[2026-04-24 06:48:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"56544ba5-de8a-4826-9181-c1f98b641c2d","trace_id":"0d23b863-4f54-4038-a81e-1e6b14b68993"}
[2026-04-24 06:48:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"56544ba5-de8a-4826-9181-c1f98b641c2d","trace_id":"0d23b863-4f54-4038-a81e-1e6b14b68993"}
[2026-04-24 06:48:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"56544ba5-de8a-4826-9181-c1f98b641c2d","trace_id":"0d23b863-4f54-4038-a81e-1e6b14b68993"}
[2026-04-24 06:48:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"57b8f721-5f9c-4555-b09f-7b30b6bb53bd","trace_id":"ac32f526-cd51-4a22-9072-6555fd1d93ce"}
[2026-04-24 06:48:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"57b8f721-5f9c-4555-b09f-7b30b6bb53bd","trace_id":"ac32f526-cd51-4a22-9072-6555fd1d93ce"}
[2026-04-24 06:48:07] local.NOTICE: Monitoring start {"correlation_id":"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516","trace_id":"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee"}
[2026-04-24 06:48:07] local.NOTICE: Monitoring end {"correlation_id":"0b7c3dc7-b6c4-472c-ae7d-ecef8343b516","trace_id":"4f7f4930-fb6f-4bba-aecd-eb890ad2b5ee"}
[2026-04-24 06:48:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"84127380-7279-44d6-862c-15b2b5cba112","trace_id":"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707"}
[2026-04-24 06:48:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"84127380-7279-44d6-862c-15b2b5cba112","trace_id":"c6aa7cf2-9521-4a71-b23b-cf1fa98ea707"}
[2026-04-24 06:48:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:11] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:11] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"48ee9d4b-28ae-434f-98c1-370869a770f3","trace_id":"24b268f4-e271-446f-9128-d2c93e688267"}
[2026-04-24 06:48:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:12] local.INFO: Running conference:monitor:count command for activities in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:12] local.INFO: [conference:monitor:count] No activities found in (2026-04-24 06:46:00, 2026-04-24 06:48:00] {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"9f6b9c79-fd3b-49e7-9569-92581a34675d","trace_id":"c29e3d24-0809-4cec-a901-51862bce4478"}
[2026-04-24 06:48:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.723} {"correlation_id":"f7242472-e47d-4264-b55d-829ff8be2c5b","trace_id":"5eebd870-6b02-484c-b745-10409d1fe0c5"}
[2026-04-24 06:48:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.723,"memoryPeakAfterCommandInMB":99.723} {"correlation_id":"f7242472-e47d-4264-b55d-829ff8be2c5b","trace_id":"5eebd870-6b02-484c-b745-10409d1fe0c5"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
62833
|
1354
|
73
|
2026-04-21T08:07:32.459298+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-21/1776 /Users/lukas/.screenpipe/data/data/2026-04-21/1776758852459_m2.jpg...
|
Firefox
|
Project Phoenix – Figma — Work
|
1
|
www.figma.com/design/jXcUe1y9mx5Fiz8KosLAUn/Projec www.figma.com/design/jXcUe1y9mx5Fiz8KosLAUn/Project-Phoenix?t=cbsLJF4RQ23Zbj5a-0...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[JY-20372] AI Reports > Empty page design and promotion - Jira
[JY-20372] AI Reports > Empty page design and promotion - Jira
Project Phoenix – Figma
Project Phoenix – Figma
Close tab
Project Phoenix – Figma
Project Phoenix – Figma
Project Phoenix – Figma
Project Phoenix – Figma
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
Pipelines - jiminny/app
Pipelines - jiminny/app
Formalize
Formalize
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Screenreader support for the board is currently disabled. To enable it via Accessibility settings, press ⌘K, type Accessibility Settings, and press Enter. To see available keyboard shortcuts, enter ⌃⇧Question mark .
Create new comment
Main menu
Minimize UI
Project Phoenix, file name
File name
Edit file menu
Pages Find
Pages
Pages
Find
❌ Cover
❌ Cover
🔍 Search
🔍 Search
🎧 OnDemand
🎧 OnDemand
▶️ Playback
▶️ Playback
💰 Deal Insights
💰 Deal Insights
🧑🤝🧑 Team Insights
🧑🤝🧑 Team Insights
📊 AI Reports
📊 AI Reports
⚙️ Org. Settings
⚙️ Org. Settings
👤 Settings - Profile
👤 Settings - Profile
👑 Kiosk
👑 Kiosk
📤 Emails
📤 Emails
🧩 Sidekick
🧩 Sidekick
🟠 Hubspot
🟠 Hubspot
Other
Other
All pages
All pages
Prototype
Prototype
Components
Components
Sandbox
Sandbox
❌ Cover
❌ Cover
🔍 Search
🔍 Search
🎧 OnDemand
🎧 OnDemand
▶️ Playback
▶️ Playback
💰 Deal Insights
💰 Deal Insights
🧑🤝🧑 Team Insights
🧑🤝🧑 Team Insights
📊 AI Reports
📊 AI Reports
⚙️ Org. Settings
⚙️ Org. Settings
👤 Settings - Profile
👤 Settings - Profile
👑 Kiosk
👑 Kiosk
📤 Emails
📤 Emails
🧩 Sidekick
🧩 Sidekick
🟠 Hubspot
🟠 Hubspot
Other
Other
All pages
All pages
Prototype
Prototype
Components
Components
Sandbox
Sandbox
Layers Collapse layers
Layers
Layers
Collapse layers
5
2
4
4
5
5
4
4
4
4
1
1
4
1
1
1
1
1
Exec Summary EU
1
Product Feedback EU
Product Feedback US
Loss Report UK
Coaching Frameworks UK
Coaching Frameworks US
Loss Report US
Exec Summary US
Exec Report Adoption - PD-190
⚠️ Don’t show the page except if the person doesn’t have a report shared with him/her
All
AA
List
7
6
Illustration
Frame
Illustration
2
Illustration
OBJECTS
1
Multiplayer tools
2
Follow lukas
Follow Galya Dimitrova
Present
Prototype view
Share
Comments
Comments
Properties
Properties
100%
100%
1
Modes
Modes
Copy Typography: Auto (Desktop)
Typography
Auto (Desktop)
Copy Variable collection: Auto (Mode 1)
Variable collection
Auto (Mode 1)
Layout
Layout
Copy
Copy Width: 1,280px
Width
1,280px
Copy Height: 832px
Height
832px
Colors
Colors
Color format
Hex
Hex
Copy
#FFFFFF
#FFFFFF
Export
Export
Add export settings
1x
Select an option
Export file type
PNG
PNG
Advanced export settings
Remove
1x
Select an option
Export file type
PNG
PNG
Advanced export settings
Remove
Export 1
Export 1
Preview
Preview
You can only view and comment on this file.
Close...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0018284575,"top":0.0518755,"width":0.07596409,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[JY-20372] AI Reports > Empty page design and promotion - Jira","depth":4,"bounds":{"left":0.0,"top":0.09497207,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20372] AI Reports > Empty page design and promotion - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.10614525,"width":0.11319814,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Project Phoenix – Figma","depth":4,"bounds":{"left":0.0,"top":0.12769353,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Project Phoenix – Figma","depth":5,"bounds":{"left":0.013297873,"top":0.13886672,"width":0.041888297,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.1348763,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Project Phoenix – Figma","depth":4,"bounds":{"left":0.0,"top":0.16041501,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Project Phoenix – Figma","depth":5,"bounds":{"left":0.013297873,"top":0.17158818,"width":0.041888297,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Project Phoenix – Figma","depth":4,"bounds":{"left":0.0,"top":0.19313647,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Project Phoenix – Figma","depth":5,"bounds":{"left":0.013297873,"top":0.20430966,"width":0.041888297,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny MCP Connector - Product - Confluence","depth":4,"bounds":{"left":0.0,"top":0.22585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny MCP Connector - Product - Confluence","depth":5,"bounds":{"left":0.013297873,"top":0.23703113,"width":0.08294548,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny Mail","depth":4,"bounds":{"left":0.0,"top":0.2585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny Mail","depth":5,"bounds":{"left":0.013297873,"top":0.2697526,"width":0.02144282,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":4,"bounds":{"left":0.0,"top":0.29130086,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.30247405,"width":0.08610372,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Feed — jiminny — Sentry","depth":4,"bounds":{"left":0.0,"top":0.32402235,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Feed — jiminny — Sentry","depth":5,"bounds":{"left":0.013297873,"top":0.33519554,"width":0.042719416,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.0,"top":0.3567438,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.013297873,"top":0.367917,"width":0.013131649,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.38946527,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.40063846,"width":0.039228722,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Formalize","depth":4,"bounds":{"left":0.0,"top":0.42218676,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Formalize","depth":5,"bounds":{"left":0.013297873,"top":0.43335995,"width":0.016788565,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.45650437,"width":0.07413564,"height":0.025538707},"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},"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},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenreader support for the board is currently disabled. To enable it via Accessibility settings, press ⌘K, type Accessibility Settings, and press Enter. To see available keyboard shortcuts, enter ⌃⇧Question mark .","depth":9,"bounds":{"left":0.07962101,"top":0.052673582,"width":0.36835107,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Create new comment","depth":11,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Main menu","depth":11,"bounds":{"left":0.08228058,"top":0.058260176,"width":0.01462766,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Minimize UI","depth":11,"bounds":{"left":0.14611037,"top":0.058260176,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Project Phoenix, file name","depth":12,"bounds":{"left":0.0852726,"top":0.087789305,"width":0.032413565,"height":0.017557861},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"File name","depth":13,"bounds":{"left":0.0852726,"top":0.08858739,"width":0.01662234,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit file menu","depth":12,"bounds":{"left":0.11968085,"top":0.08699122,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Pages Find","depth":12,"bounds":{"left":0.07962101,"top":0.11652035,"width":0.07978723,"height":0.031923383},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXButton","text":"Pages","depth":13,"bounds":{"left":0.07962101,"top":0.11652035,"width":0.069148935,"height":0.031923383},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXStaticText","text":"Pages","depth":16,"bounds":{"left":0.08494016,"top":0.12689546,"width":0.010970744,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Find","depth":13,"bounds":{"left":0.14876994,"top":0.12290503,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"❌ Cover","depth":18,"bounds":{"left":0.08228058,"top":0.15163608,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"❌ Cover","depth":21,"bounds":{"left":0.08494016,"top":0.15562649,"width":0.015292553,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"🔍 Search","depth":18,"bounds":{"left":0.08228058,"top":0.17717478,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"🔍 Search","depth":21,"bounds":{"left":0.08494016,"top":0.1811652,"width":0.017121011,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"🎧 OnDemand","depth":18,"bounds":{"left":0.08228058,"top":0.20271349,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"🎧 OnDemand","depth":21,"bounds":{"left":0.08494016,"top":0.20670392,"width":0.02443484,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"▶️ Playback","depth":18,"bounds":{"left":0.08228058,"top":0.22825219,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"▶️ Playback","depth":21,"bounds":{"left":0.08494016,"top":0.23224261,"width":0.021609042,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"💰 Deal Insights","depth":18,"bounds":{"left":0.08228058,"top":0.25379092,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"💰 Deal Insights","depth":21,"bounds":{"left":0.08494016,"top":0.25778133,"width":0.027260639,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"🧑🤝🧑 Team Insights","depth":18,"bounds":{"left":0.08228058,"top":0.2793296,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"🧑🤝🧑 Team Insights","depth":21,"bounds":{"left":0.08494016,"top":0.28332004,"width":0.029920213,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"📊 AI Reports","depth":18,"bounds":{"left":0.08228058,"top":0.3048683,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"📊 AI Reports","depth":21,"bounds":{"left":0.08494016,"top":0.30885875,"width":0.0234375,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⚙️ Org. Settings","depth":18,"bounds":{"left":0.08228058,"top":0.33040702,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"⚙️ Org. Settings","depth":21,"bounds":{"left":0.08494016,"top":0.33439744,"width":0.028590426,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"👤 Settings - Profile","depth":18,"bounds":{"left":0.08228058,"top":0.35594574,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"👤 Settings - Profile","depth":21,"bounds":{"left":0.08494016,"top":0.35993615,"width":0.03507314,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"👑 Kiosk","depth":18,"bounds":{"left":0.08228058,"top":0.38148445,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"👑 Kiosk","depth":21,"bounds":{"left":0.08494016,"top":0.38547486,"width":0.014461436,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"📤 Emails","depth":18,"bounds":{"left":0.08228058,"top":0.40702313,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"📤 Emails","depth":21,"bounds":{"left":0.08494016,"top":0.41101357,"width":0.016123671,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"🧩 Sidekick","depth":18,"bounds":{"left":0.08228058,"top":0.43256184,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"🧩 Sidekick","depth":21,"bounds":{"left":0.08494016,"top":0.4365523,"width":0.019448139,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"🟠 Hubspot","depth":18,"bounds":{"left":0.08228058,"top":0.45810056,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"🟠 Hubspot","depth":21,"bounds":{"left":0.08494016,"top":0.46209097,"width":0.019780586,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Other","depth":18,"bounds":{"left":0.08228058,"top":0.48363927,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Other","depth":21,"bounds":{"left":0.08494016,"top":0.48762968,"width":0.009807181,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"All pages","depth":18,"bounds":{"left":0.08228058,"top":0.509178,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All pages","depth":21,"bounds":{"left":0.08494016,"top":0.5131684,"width":0.015791224,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Prototype","depth":18,"bounds":{"left":0.08228058,"top":0.53471667,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Prototype","depth":21,"bounds":{"left":0.08494016,"top":0.5387071,"width":0.016954787,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Components","depth":18,"bounds":{"left":0.08228058,"top":0.5602554,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Components","depth":21,"bounds":{"left":0.08494016,"top":0.5642458,"width":0.022107713,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Sandbox","depth":18,"bounds":{"left":0.08228058,"top":0.5857941,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Sandbox","depth":21,"bounds":{"left":0.08494016,"top":0.5897845,"width":0.01512633,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"❌ Cover","depth":18,"bounds":{"left":0.08228058,"top":0.15163608,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"❌ Cover","depth":21,"bounds":{"left":0.08494016,"top":0.15562649,"width":0.015292553,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"🔍 Search","depth":18,"bounds":{"left":0.08228058,"top":0.17717478,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"🔍 Search","depth":21,"bounds":{"left":0.08494016,"top":0.1811652,"width":0.017121011,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"🎧 OnDemand","depth":18,"bounds":{"left":0.08228058,"top":0.20271349,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"🎧 OnDemand","depth":21,"bounds":{"left":0.08494016,"top":0.20670392,"width":0.02443484,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"▶️ Playback","depth":18,"bounds":{"left":0.08228058,"top":0.22825219,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"▶️ Playback","depth":21,"bounds":{"left":0.08494016,"top":0.23224261,"width":0.021609042,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"💰 Deal Insights","depth":18,"bounds":{"left":0.08228058,"top":0.25379092,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"💰 Deal Insights","depth":21,"bounds":{"left":0.08494016,"top":0.25778133,"width":0.027260639,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"🧑🤝🧑 Team Insights","depth":18,"bounds":{"left":0.08228058,"top":0.2793296,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"🧑🤝🧑 Team Insights","depth":21,"bounds":{"left":0.08494016,"top":0.28332004,"width":0.029920213,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"📊 AI Reports","depth":18,"bounds":{"left":0.08228058,"top":0.3048683,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"📊 AI Reports","depth":21,"bounds":{"left":0.08494016,"top":0.30885875,"width":0.0234375,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⚙️ Org. Settings","depth":18,"bounds":{"left":0.08228058,"top":0.33040702,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"⚙️ Org. Settings","depth":21,"bounds":{"left":0.08494016,"top":0.33439744,"width":0.028590426,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"👤 Settings - Profile","depth":18,"bounds":{"left":0.08228058,"top":0.35594574,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"👤 Settings - Profile","depth":21,"bounds":{"left":0.08494016,"top":0.35993615,"width":0.03507314,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"👑 Kiosk","depth":18,"bounds":{"left":0.08228058,"top":0.38148445,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"👑 Kiosk","depth":21,"bounds":{"left":0.08494016,"top":0.38547486,"width":0.014461436,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"📤 Emails","depth":18,"bounds":{"left":0.08228058,"top":0.40702313,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"📤 Emails","depth":21,"bounds":{"left":0.08494016,"top":0.41101357,"width":0.016123671,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"🧩 Sidekick","depth":18,"bounds":{"left":0.08228058,"top":0.43256184,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"🧩 Sidekick","depth":21,"bounds":{"left":0.08494016,"top":0.4365523,"width":0.019448139,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"🟠 Hubspot","depth":18,"bounds":{"left":0.08228058,"top":0.45810056,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"🟠 Hubspot","depth":21,"bounds":{"left":0.08494016,"top":0.46209097,"width":0.019780586,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Other","depth":18,"bounds":{"left":0.08228058,"top":0.48363927,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Other","depth":21,"bounds":{"left":0.08494016,"top":0.48762968,"width":0.009807181,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"All pages","depth":18,"bounds":{"left":0.08228058,"top":0.509178,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All pages","depth":21,"bounds":{"left":0.08494016,"top":0.5131684,"width":0.015791224,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Prototype","depth":18,"bounds":{"left":0.08228058,"top":0.53471667,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Prototype","depth":21,"bounds":{"left":0.08494016,"top":0.5387071,"width":0.016954787,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Components","depth":18,"bounds":{"left":0.08228058,"top":0.5602554,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Components","depth":21,"bounds":{"left":0.08494016,"top":0.5642458,"width":0.022107713,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Sandbox","depth":18,"bounds":{"left":0.08228058,"top":0.5857941,"width":0.07446808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Sandbox","depth":21,"bounds":{"left":0.08494016,"top":0.5897845,"width":0.01512633,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Layers Collapse layers","depth":13,"bounds":{"left":0.07962101,"top":0.36073422,"width":0.07978723,"height":0.031923383},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXButton","text":"Layers","depth":14,"bounds":{"left":0.07962101,"top":0.36073422,"width":0.069148935,"height":0.031923383},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXStaticText","text":"Layers","depth":16,"bounds":{"left":0.08494016,"top":0.37110934,"width":0.011801862,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Collapse layers","depth":14,"bounds":{"left":0.14876994,"top":0.36711892,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5","depth":19,"bounds":{"left":0.09291888,"top":0.0,"width":0.0023271276,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":19,"bounds":{"left":0.09291888,"top":0.0,"width":0.0023271276,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4","depth":19,"bounds":{"left":0.09291888,"top":0.0,"width":0.002493351,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4","depth":19,"bounds":{"left":0.09291888,"top":0.0,"width":0.002493351,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5","depth":19,"bounds":{"left":0.09291888,"top":0.017557861,"width":0.0023271276,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5","depth":19,"bounds":{"left":0.09291888,"top":0.04309657,"width":0.0023271276,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4","depth":19,"bounds":{"left":0.09291888,"top":0.06863528,"width":0.002493351,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4","depth":19,"bounds":{"left":0.09291888,"top":0.09417398,"width":0.002493351,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4","depth":19,"bounds":{"left":0.09291888,"top":0.11971269,"width":0.002493351,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4","depth":19,"bounds":{"left":0.09291888,"top":0.1452514,"width":0.002493351,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":19,"bounds":{"left":0.09291888,"top":0.1707901,"width":0.0018284575,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":19,"bounds":{"left":0.09291888,"top":0.1963288,"width":0.0018284575,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4","depth":19,"bounds":{"left":0.09291888,"top":0.22186752,"width":0.002493351,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":19,"bounds":{"left":0.09291888,"top":0.24740623,"width":0.0018284575,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":19,"bounds":{"left":0.09291888,"top":0.27294493,"width":0.0018284575,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":19,"bounds":{"left":0.09291888,"top":0.29848364,"width":0.0018284575,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":19,"bounds":{"left":0.09291888,"top":0.32402235,"width":0.0018284575,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":19,"bounds":{"left":0.09291888,"top":0.34956107,"width":0.0018284575,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Exec Summary EU","depth":19,"bounds":{"left":0.09291888,"top":0.37509975,"width":0.032413565,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":19,"bounds":{"left":0.09291888,"top":0.40063846,"width":0.0018284575,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Product Feedback EU","depth":19,"bounds":{"left":0.09291888,"top":0.42617717,"width":0.038231384,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Product Feedback US","depth":19,"bounds":{"left":0.09291888,"top":0.4517159,"width":0.038397606,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Loss Report UK","depth":19,"bounds":{"left":0.09291888,"top":0.4772546,"width":0.027426861,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Coaching Frameworks UK","depth":19,"bounds":{"left":0.09291888,"top":0.5027933,"width":0.046043884,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Coaching Frameworks US","depth":19,"bounds":{"left":0.09291888,"top":0.528332,"width":0.045877658,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Loss Report US","depth":19,"bounds":{"left":0.09291888,"top":0.55387074,"width":0.027426861,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Exec Summary US","depth":19,"bounds":{"left":0.09291888,"top":0.5794094,"width":0.032413565,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Exec Report Adoption - PD-190","depth":19,"bounds":{"left":0.09291888,"top":0.6049481,"width":0.055684842,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"⚠️ Don’t show the page except if the person doesn’t have a report shared with him/her","depth":19,"bounds":{"left":0.09291888,"top":0.8603352,"width":0.15076463,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"All","depth":19,"bounds":{"left":0.09291888,"top":0.8858739,"width":0.0048204786,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"AA","depth":19,"bounds":{"left":0.09291888,"top":0.9114126,"width":0.005319149,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"List","depth":19,"bounds":{"left":0.09291888,"top":0.93695134,"width":0.006482713,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7","depth":19,"bounds":{"left":0.1008976,"top":0.63048685,"width":0.0021609042,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"6","depth":19,"bounds":{"left":0.1008976,"top":0.6560255,"width":0.0023271276,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Illustration","depth":19,"bounds":{"left":0.1008976,"top":0.6815643,"width":0.019115692,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frame","depth":19,"bounds":{"left":0.1008976,"top":0.70710295,"width":0.010970744,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Illustration","depth":19,"bounds":{"left":0.1008976,"top":0.73264164,"width":0.019115692,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":19,"bounds":{"left":0.1008976,"top":0.7581804,"width":0.0021609042,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Illustration","depth":19,"bounds":{"left":0.1008976,"top":0.78371906,"width":0.019115692,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"OBJECTS","depth":19,"bounds":{"left":0.1008976,"top":0.8092578,"width":0.016788565,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":19,"bounds":{"left":0.1008976,"top":0.8347965,"width":0.0016622341,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Multiplayer tools","depth":12,"bounds":{"left":0.9388298,"top":0.061452515,"width":0.016954787,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":14,"bounds":{"left":0.94763964,"top":0.06584198,"width":0.0021609042,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Follow lukas","depth":13,"bounds":{"left":0.9311835,"top":0.061452515,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Follow Galya Dimitrova","depth":13,"bounds":{"left":0.92386967,"top":0.061452515,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Present","depth":12,"bounds":{"left":0.9616024,"top":0.058260176,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Prototype view","depth":12,"bounds":{"left":0.97257316,"top":0.058260176,"width":0.005319149,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share","depth":11,"bounds":{"left":0.97922206,"top":0.058260176,"width":0.018118352,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Comments","depth":12,"bounds":{"left":0.92287236,"top":0.090183556,"width":0.024601065,"height":0.01915403},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Comments","depth":14,"bounds":{"left":0.92569816,"top":0.09417398,"width":0.018949468,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Properties","depth":12,"bounds":{"left":0.9488032,"top":0.090183556,"width":0.023769947,"height":0.01915403},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Properties","depth":14,"bounds":{"left":0.95146275,"top":0.09417398,"width":0.018450798,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"100%","depth":12,"bounds":{"left":0.9773936,"top":0.090183556,"width":0.019946808,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"100%","depth":13,"bounds":{"left":0.98138297,"top":0.09417398,"width":0.00930851,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"1","depth":13,"bounds":{"left":0.9321808,"top":0.122505985,"width":0.004488032,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Modes","depth":16,"bounds":{"left":0.9255319,"top":0.15802075,"width":0.012134309,"height":0.012769354},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Modes","depth":17,"bounds":{"left":0.9255319,"top":0.15881884,"width":0.012134309,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy Typography: Auto (Desktop)","depth":15,"bounds":{"left":0.92420214,"top":0.18036711,"width":0.07047872,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Typography","depth":18,"bounds":{"left":0.9255319,"top":0.18435754,"width":0.020944148,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auto (Desktop)","depth":18,"bounds":{"left":0.9574468,"top":0.18435754,"width":0.026595745,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy Variable collection: Auto (Mode 1)","depth":15,"bounds":{"left":0.92420214,"top":0.19952115,"width":0.07047872,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Variable collection","depth":18,"bounds":{"left":0.9255319,"top":0.20351157,"width":0.032247342,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auto (Mode 1)","depth":18,"bounds":{"left":0.9574468,"top":0.20351157,"width":0.024767287,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Layout","depth":16,"bounds":{"left":0.9255319,"top":0.2386273,"width":0.012134309,"height":0.012769354},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Layout","depth":17,"bounds":{"left":0.9255319,"top":0.23942538,"width":0.012134309,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":15,"bounds":{"left":0.9893617,"top":0.23543495,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Copy Width: 1,280px","depth":15,"bounds":{"left":0.92420214,"top":0.26097366,"width":0.07047872,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Width","depth":18,"bounds":{"left":0.9255319,"top":0.26496407,"width":0.010305851,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1,280px","depth":18,"bounds":{"left":0.9574468,"top":0.26496407,"width":0.013630319,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy Height: 832px","depth":15,"bounds":{"left":0.92420214,"top":0.2801277,"width":0.07047872,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Height","depth":18,"bounds":{"left":0.9255319,"top":0.28411812,"width":0.011635638,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"832px","depth":18,"bounds":{"left":0.9574468,"top":0.28411812,"width":0.011136968,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Colors","depth":16,"bounds":{"left":0.9255319,"top":0.31923383,"width":0.011635638,"height":0.012769354},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Colors","depth":17,"bounds":{"left":0.9255319,"top":0.3200319,"width":0.011635638,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Color format","depth":16,"bounds":{"left":0.9695811,"top":0.3160415,"width":0.022273935,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXComboBox","text":"Hex","depth":15,"bounds":{"left":0.96991354,"top":0.3160415,"width":0.018118352,"height":0.01915403},"value":"Hex","help_text":"","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Hex","depth":17,"bounds":{"left":0.9729056,"top":0.3196329,"width":0.0068151597,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":15,"bounds":{"left":0.9893617,"top":0.3160415,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"#FFFFFF","depth":15,"bounds":{"left":0.92021275,"top":0.3415802,"width":0.07712766,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#FFFFFF","depth":17,"bounds":{"left":0.9361702,"top":0.34876296,"width":0.015292553,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Export","depth":15,"bounds":{"left":0.9255319,"top":0.377494,"width":0.06382979,"height":0.031923383},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Export","depth":16,"bounds":{"left":0.9255319,"top":0.38786912,"width":0.011801862,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add export settings","depth":15,"bounds":{"left":0.9893617,"top":0.38387868,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXTextField","text":"1x","depth":20,"bounds":{"left":0.9255319,"top":0.41260973,"width":0.016289894,"height":0.01915403},"value":"1x","role_description":"text field","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Select an option","depth":20,"bounds":{"left":0.9421542,"top":0.41260973,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Export file type","depth":20,"bounds":{"left":0.9524601,"top":0.41260973,"width":0.02642952,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXComboBox","text":"PNG","depth":19,"bounds":{"left":0.9527925,"top":0.41260973,"width":0.024601065,"height":0.01915403},"value":"PNG","help_text":"","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"PNG","depth":21,"bounds":{"left":0.95578456,"top":0.4162011,"width":0.0078125,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Advanced export settings","depth":19,"bounds":{"left":0.9800532,"top":0.41260973,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Remove","depth":19,"bounds":{"left":0.9893617,"top":0.41260973,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXTextField","text":"1x","depth":20,"bounds":{"left":0.9255319,"top":0.41260973,"width":0.016289894,"height":0.01915403},"value":"1x","role_description":"text field","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Select an option","depth":20,"bounds":{"left":0.9421542,"top":0.41260973,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Export file type","depth":20,"bounds":{"left":0.9524601,"top":0.41260973,"width":0.02642952,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXComboBox","text":"PNG","depth":19,"bounds":{"left":0.9527925,"top":0.41260973,"width":0.024601065,"height":0.01915403},"value":"PNG","help_text":"","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"PNG","depth":21,"bounds":{"left":0.95578456,"top":0.4162011,"width":0.0078125,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Advanced export settings","depth":19,"bounds":{"left":0.9800532,"top":0.41260973,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Remove","depth":19,"bounds":{"left":0.9893617,"top":0.41260973,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Export 1","depth":16,"bounds":{"left":0.9255319,"top":0.43814844,"width":0.069148935,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Export 1","depth":20,"bounds":{"left":0.95295876,"top":0.44213888,"width":0.014295213,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Preview","depth":17,"bounds":{"left":0.92287236,"top":0.4668795,"width":0.019448139,"height":0.012769354},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Preview","depth":19,"bounds":{"left":0.9281915,"top":0.46767756,"width":0.01412899,"height":0.011173184},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"You can only view and comment on this file.","depth":12,"bounds":{"left":0.4808843,"top":0.92098963,"width":0.090259306,"height":0.0131683955},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close","depth":12,"bounds":{"left":0.60139626,"top":0.91779727,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-4441505438361906815
|
8401390206757725611
|
visual_change
|
hybrid
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[JY-20372] AI Reports > Empty page design and promotion - Jira
[JY-20372] AI Reports > Empty page design and promotion - Jira
Project Phoenix – Figma
Project Phoenix – Figma
Close tab
Project Phoenix – Figma
Project Phoenix – Figma
Project Phoenix – Figma
Project Phoenix – Figma
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
Pipelines - jiminny/app
Pipelines - jiminny/app
Formalize
Formalize
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Screenreader support for the board is currently disabled. To enable it via Accessibility settings, press ⌘K, type Accessibility Settings, and press Enter. To see available keyboard shortcuts, enter ⌃⇧Question mark .
Create new comment
Main menu
Minimize UI
Project Phoenix, file name
File name
Edit file menu
Pages Find
Pages
Pages
Find
❌ Cover
❌ Cover
🔍 Search
🔍 Search
🎧 OnDemand
🎧 OnDemand
▶️ Playback
▶️ Playback
💰 Deal Insights
💰 Deal Insights
🧑🤝🧑 Team Insights
🧑🤝🧑 Team Insights
📊 AI Reports
📊 AI Reports
⚙️ Org. Settings
⚙️ Org. Settings
👤 Settings - Profile
👤 Settings - Profile
👑 Kiosk
👑 Kiosk
📤 Emails
📤 Emails
🧩 Sidekick
🧩 Sidekick
🟠 Hubspot
🟠 Hubspot
Other
Other
All pages
All pages
Prototype
Prototype
Components
Components
Sandbox
Sandbox
❌ Cover
❌ Cover
🔍 Search
🔍 Search
🎧 OnDemand
🎧 OnDemand
▶️ Playback
▶️ Playback
💰 Deal Insights
💰 Deal Insights
🧑🤝🧑 Team Insights
🧑🤝🧑 Team Insights
📊 AI Reports
📊 AI Reports
⚙️ Org. Settings
⚙️ Org. Settings
👤 Settings - Profile
👤 Settings - Profile
👑 Kiosk
👑 Kiosk
📤 Emails
📤 Emails
🧩 Sidekick
🧩 Sidekick
🟠 Hubspot
🟠 Hubspot
Other
Other
All pages
All pages
Prototype
Prototype
Components
Components
Sandbox
Sandbox
Layers Collapse layers
Layers
Layers
Collapse layers
5
2
4
4
5
5
4
4
4
4
1
1
4
1
1
1
1
1
Exec Summary EU
1
Product Feedback EU
Product Feedback US
Loss Report UK
Coaching Frameworks UK
Coaching Frameworks US
Loss Report US
Exec Summary US
Exec Report Adoption - PD-190
⚠️ Don’t show the page except if the person doesn’t have a report shared with him/her
All
AA
List
7
6
Illustration
Frame
Illustration
2
Illustration
OBJECTS
1
Multiplayer tools
2
Follow lukas
Follow Galya Dimitrova
Present
Prototype view
Share
Comments
Comments
Properties
Properties
100%
100%
1
Modes
Modes
Copy Typography: Auto (Desktop)
Typography
Auto (Desktop)
Copy Variable collection: Auto (Mode 1)
Variable collection
Auto (Mode 1)
Layout
Layout
Copy
Copy Width: 1,280px
Width
1,280px
Copy Height: 832px
Height
832px
Colors
Colors
Color format
Hex
Hex
Copy
#FFFFFF
#FFFFFF
Export
Export
Add export settings
1x
Select an option
Export file type
PNG
PNG
Advanced export settings
Remove
1x
Select an option
Export file type
PNG
PNG
Advanced export settings
Remove
Export 1
Export 1
Preview
Preview
You can only view and comment on this file.
Close
FirefoxcalVIewMistorbookmarksProtllesToolsWindowmelp• • www.figma.com/design/jXcU1y9mx5Fiz8KosLAUn/Project-Phoenix?t=cbsLJF4RQ23Zbj5a-0Exec Report Adoption - PD-190Prolect Phoenix[JY-20372] Al Reports > Empty paProject Phoenix - Figma• Search8 Project Phoenix - FigmaOnDemand* Jiminny MCP Connector - ProductPlaybacke Deal InsighisJiminny Mail4 Team Insiahts(UY-20500) Batch initial sync for Songoing insightsFeed — jiminny — Sentry© Org. Settings8 Jiminnyo automated reports.E dates straight to your inbox.@ Pipelines - jiminny/app#1I Product Feedback EU* Formalize)# Product Feedback US- New Tab# Loss Report UK" Coachina Frameworks UkCoachina Frameworks US# Loss Report USit Exec Summary USPl Exec Renort Adontion - PD-190#7tit Tllustrationort# FrameIllustration# Illustrationtt OBJECTSTA Don't show the page except if the1AllPAA#List$g+Al ReportsTurn your questions intoongoing insightsTurn your Ask Jiminny questions into automated reports.Track specific topics over time and get updates straight toyour inbox.Track coaching progress over timeFolloyc their progress wek by week without reviewingFollow a specinc trend or questioniTrack things like objections, competitors, or feedbackacross your calls.Create Your First ReportMonitor changes in messaging or productSee how customers respond and whether your team isStoo reveating the same analvsisAstomase the queto ons you ask regulanty and get the1280 x 832QCreate Report‹40f Support Daily - in 3h 53m100% LzTue 21 Apr 11:07:329882comments100%v# 1Auto (Desktop)Variable collecti.. Auto (Mode 1)Widtl1,280pxHeight8320xHexyO #FFFFFFExportOBJECTSExport 1...
|
62832
|
|
76730
|
1923
|
38
|
2026-04-24T08:19:01.549793+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777018741549_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Support\Collection;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Team;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
class AutomatedReportsCommand extends Command
{
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[automated-reports]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports
{--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).
Use --report-id to manually trigger a specific report by ID or UUID.';
public function __construct(
private readonly LoggerInterface $logger,
private readonly BusDispatcher $dispatcher,
private readonly AutomatedReportsRepository $reportRepository
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->logger->info(self::LOG_PREFIX . ' Started');
$this->disableExpiredAskJiminnyReports();
$now = Carbon::now();
$isMonday = $now->isMonday();
$isFirstDayOfMonth = $now->day === 1;
$currentMonth = $now->month;
// Check if the current month is a quarterly month (January, April, July, October)
$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);
$this->logger->info(self::LOG_PREFIX . ' Checking conditions', [
'isMonday' => $isMonday,
'isFirstDayOfMonth' => $isFirstDayOfMonth,
'currentMonth' => $currentMonth,
'isQuarterlyMonth' => $isQuarterlyMonth,
]);
// Process daily reports
$this->processReports(AutomatedReportsService::FREQUENCY_DAILY);
// Process weekly reports on Mondays
if ($isMonday) {
$this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);
}
// Process monthly reports on the first day of the month
if ($isFirstDayOfMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);
}
// Process quarterly reports on the first day of January, April, July, and October
if ($isFirstDayOfMonth && $isQuarterlyMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);
}
$this->logger->info(self::LOG_PREFIX . ' Completed');
return 0;
}
private function disableExpiredAskJiminnyReports(): void
{
$expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();
foreach ($expiredReports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->reportRepository->update($report, ['status' => false]);
}
}
/**
* Process reports for a specific frequency.
*
* @param string $frequency
*
* @return void
*/
private function processReports(string $frequency): void
{
$this->logger->info(self::LOG_PREFIX . " Processing $frequency reports");
$reportId = $this->option('report-id');
if ($reportId !== null) {
$reports = $this->getReportById($reportId);
} else {
// Get all enabled, not deleted reports with active teams for the specified frequency
$reports = $this->reportRepository->getActiveReportsByFrequency($frequency);
}
$this->logger->info(self::LOG_PREFIX . " Found {$reports->count()} $frequency reports to process");
/** @var AutomatedReport $report */
foreach ($reports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'frequency' => $report->getFrequency(),
'type' => $report->getType(),
]);
$job = $report->isAskJiminnyReport()
? new RequestGenerateAskJiminnyReportJob($report->getUuid())
: new RequestGenerateReportJob($report->getUuid());
$this->dispatcher->dispatch($job);
}
}
private function getReportById(string $reportId): Collection
{
$report = $this->reportRepository->findByIdOrUuid($reportId);
if ($report === null) {
$this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);
$this->warn("Report not found: {$reportId}");
return collect();
}
if (! $report->getStatus()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
]);
$this->warn('Report is inactive — processing anyway (manual override).');
}
$team = $report->getTeam();
if ($team->getStatus() !== Team::STATUS_ACTIVE) {
$this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'teamStatus' => $team->getStatus(),
]);
$this->warn("Team #{$report->getTeamId()} is not active — processing anyway (manual override).");
}
if ($report->isExpired()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()
. ') — processing anyway (manual override).');
}
$this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());
return collect([$report]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20738-debug-AJ-tracking-UP, menu","depth":5,"help_text":"Git Branch: JY-20738-debug-AJ-tracking-UP","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports\n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).\n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $this->disableExpiredAskJiminnyReports();\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n private function disableExpiredAskJiminnyReports(): void\n {\n $expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();\n\n foreach ($expiredReports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n\n $this->reportRepository->update($report, ['status' => false]);\n }\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()\n . ') — processing anyway (manual override).');\n }\n\n $this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());\n\n return collect([$report]);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports\n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).\n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $this->disableExpiredAskJiminnyReports();\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n private function disableExpiredAskJiminnyReports(): void\n {\n $expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();\n\n foreach ($expiredReports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n\n $this->reportRepository->update($report, ['status' => false]);\n }\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()\n . ') — processing anyway (manual override).');\n }\n\n $this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());\n\n return collect([$report]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4713233747762993819
|
8401046760627731248
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Support\Collection;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Team;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
class AutomatedReportsCommand extends Command
{
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[automated-reports]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports
{--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).
Use --report-id to manually trigger a specific report by ID or UUID.';
public function __construct(
private readonly LoggerInterface $logger,
private readonly BusDispatcher $dispatcher,
private readonly AutomatedReportsRepository $reportRepository
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->logger->info(self::LOG_PREFIX . ' Started');
$this->disableExpiredAskJiminnyReports();
$now = Carbon::now();
$isMonday = $now->isMonday();
$isFirstDayOfMonth = $now->day === 1;
$currentMonth = $now->month;
// Check if the current month is a quarterly month (January, April, July, October)
$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);
$this->logger->info(self::LOG_PREFIX . ' Checking conditions', [
'isMonday' => $isMonday,
'isFirstDayOfMonth' => $isFirstDayOfMonth,
'currentMonth' => $currentMonth,
'isQuarterlyMonth' => $isQuarterlyMonth,
]);
// Process daily reports
$this->processReports(AutomatedReportsService::FREQUENCY_DAILY);
// Process weekly reports on Mondays
if ($isMonday) {
$this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);
}
// Process monthly reports on the first day of the month
if ($isFirstDayOfMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);
}
// Process quarterly reports on the first day of January, April, July, and October
if ($isFirstDayOfMonth && $isQuarterlyMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);
}
$this->logger->info(self::LOG_PREFIX . ' Completed');
return 0;
}
private function disableExpiredAskJiminnyReports(): void
{
$expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();
foreach ($expiredReports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->reportRepository->update($report, ['status' => false]);
}
}
/**
* Process reports for a specific frequency.
*
* @param string $frequency
*
* @return void
*/
private function processReports(string $frequency): void
{
$this->logger->info(self::LOG_PREFIX . " Processing $frequency reports");
$reportId = $this->option('report-id');
if ($reportId !== null) {
$reports = $this->getReportById($reportId);
} else {
// Get all enabled, not deleted reports with active teams for the specified frequency
$reports = $this->reportRepository->getActiveReportsByFrequency($frequency);
}
$this->logger->info(self::LOG_PREFIX . " Found {$reports->count()} $frequency reports to process");
/** @var AutomatedReport $report */
foreach ($reports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'frequency' => $report->getFrequency(),
'type' => $report->getType(),
]);
$job = $report->isAskJiminnyReport()
? new RequestGenerateAskJiminnyReportJob($report->getUuid())
: new RequestGenerateReportJob($report->getUuid());
$this->dispatcher->dispatch($job);
}
}
private function getReportById(string $reportId): Collection
{
$report = $this->reportRepository->findByIdOrUuid($reportId);
if ($report === null) {
$this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);
$this->warn("Report not found: {$reportId}");
return collect();
}
if (! $report->getStatus()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
]);
$this->warn('Report is inactive — processing anyway (manual override).');
}
$team = $report->getTeam();
if ($team->getStatus() !== Team::STATUS_ACTIVE) {
$this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'teamStatus' => $team->getStatus(),
]);
$this->warn("Team #{$report->getTeamId()} is not active — processing anyway (manual override).");
}
if ($report->isExpired()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()
. ') — processing anyway (manual override).');
}
$this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());
return collect([$report]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
76728
|
|
76731
|
1924
|
35
|
2026-04-24T08:19:01.549795+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777018741549_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Support\Collection;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Team;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
class AutomatedReportsCommand extends Command
{
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[automated-reports]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports
{--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).
Use --report-id to manually trigger a specific report by ID or UUID.';
public function __construct(
private readonly LoggerInterface $logger,
private readonly BusDispatcher $dispatcher,
private readonly AutomatedReportsRepository $reportRepository
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->logger->info(self::LOG_PREFIX . ' Started');
$this->disableExpiredAskJiminnyReports();
$now = Carbon::now();
$isMonday = $now->isMonday();
$isFirstDayOfMonth = $now->day === 1;
$currentMonth = $now->month;
// Check if the current month is a quarterly month (January, April, July, October)
$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);
$this->logger->info(self::LOG_PREFIX . ' Checking conditions', [
'isMonday' => $isMonday,
'isFirstDayOfMonth' => $isFirstDayOfMonth,
'currentMonth' => $currentMonth,
'isQuarterlyMonth' => $isQuarterlyMonth,
]);
// Process daily reports
$this->processReports(AutomatedReportsService::FREQUENCY_DAILY);
// Process weekly reports on Mondays
if ($isMonday) {
$this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);
}
// Process monthly reports on the first day of the month
if ($isFirstDayOfMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);
}
// Process quarterly reports on the first day of January, April, July, and October
if ($isFirstDayOfMonth && $isQuarterlyMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);
}
$this->logger->info(self::LOG_PREFIX . ' Completed');
return 0;
}
private function disableExpiredAskJiminnyReports(): void
{
$expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();
foreach ($expiredReports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->reportRepository->update($report, ['status' => false]);
}
}
/**
* Process reports for a specific frequency.
*
* @param string $frequency
*
* @return void
*/
private function processReports(string $frequency): void
{
$this->logger->info(self::LOG_PREFIX . " Processing $frequency reports");
$reportId = $this->option('report-id');
if ($reportId !== null) {
$reports = $this->getReportById($reportId);
} else {
// Get all enabled, not deleted reports with active teams for the specified frequency
$reports = $this->reportRepository->getActiveReportsByFrequency($frequency);
}
$this->logger->info(self::LOG_PREFIX . " Found {$reports->count()} $frequency reports to process");
/** @var AutomatedReport $report */
foreach ($reports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'frequency' => $report->getFrequency(),
'type' => $report->getType(),
]);
$job = $report->isAskJiminnyReport()
? new RequestGenerateAskJiminnyReportJob($report->getUuid())
: new RequestGenerateReportJob($report->getUuid());
$this->dispatcher->dispatch($job);
}
}
private function getReportById(string $reportId): Collection
{
$report = $this->reportRepository->findByIdOrUuid($reportId);
if ($report === null) {
$this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);
$this->warn("Report not found: {$reportId}");
return collect();
}
if (! $report->getStatus()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
]);
$this->warn('Report is inactive — processing anyway (manual override).');
}
$team = $report->getTeam();
if ($team->getStatus() !== Team::STATUS_ACTIVE) {
$this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'teamStatus' => $team->getStatus(),
]);
$this->warn("Team #{$report->getTeamId()} is not active — processing anyway (manual override).");
}
if ($report->isExpired()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()
. ') — processing anyway (manual override).');
}
$this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());
return collect([$report]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20738-debug-AJ-tracking-UP, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.08510638,"height":0.025538707},"help_text":"Git Branch: JY-20738-debug-AJ-tracking-UP","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.62799203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.63796544,"top":0.12529927,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6476064,"top":0.123703115,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.6549202,"top":0.123703115,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports\n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).\n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $this->disableExpiredAskJiminnyReports();\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n private function disableExpiredAskJiminnyReports(): void\n {\n $expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();\n\n foreach ($expiredReports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n\n $this->reportRepository->update($report, ['status' => false]);\n }\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()\n . ') — processing anyway (manual override).');\n }\n\n $this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());\n\n return collect([$report]);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports\n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).\n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $this->disableExpiredAskJiminnyReports();\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n private function disableExpiredAskJiminnyReports(): void\n {\n $expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();\n\n foreach ($expiredReports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n\n $this->reportRepository->update($report, ['status' => false]);\n }\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()\n . ') — processing anyway (manual override).');\n }\n\n $this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());\n\n return collect([$report]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4713233747762993819
|
8401046760627731248
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Support\Collection;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Team;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
class AutomatedReportsCommand extends Command
{
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[automated-reports]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports
{--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).
Use --report-id to manually trigger a specific report by ID or UUID.';
public function __construct(
private readonly LoggerInterface $logger,
private readonly BusDispatcher $dispatcher,
private readonly AutomatedReportsRepository $reportRepository
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->logger->info(self::LOG_PREFIX . ' Started');
$this->disableExpiredAskJiminnyReports();
$now = Carbon::now();
$isMonday = $now->isMonday();
$isFirstDayOfMonth = $now->day === 1;
$currentMonth = $now->month;
// Check if the current month is a quarterly month (January, April, July, October)
$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);
$this->logger->info(self::LOG_PREFIX . ' Checking conditions', [
'isMonday' => $isMonday,
'isFirstDayOfMonth' => $isFirstDayOfMonth,
'currentMonth' => $currentMonth,
'isQuarterlyMonth' => $isQuarterlyMonth,
]);
// Process daily reports
$this->processReports(AutomatedReportsService::FREQUENCY_DAILY);
// Process weekly reports on Mondays
if ($isMonday) {
$this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);
}
// Process monthly reports on the first day of the month
if ($isFirstDayOfMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);
}
// Process quarterly reports on the first day of January, April, July, and October
if ($isFirstDayOfMonth && $isQuarterlyMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);
}
$this->logger->info(self::LOG_PREFIX . ' Completed');
return 0;
}
private function disableExpiredAskJiminnyReports(): void
{
$expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();
foreach ($expiredReports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->reportRepository->update($report, ['status' => false]);
}
}
/**
* Process reports for a specific frequency.
*
* @param string $frequency
*
* @return void
*/
private function processReports(string $frequency): void
{
$this->logger->info(self::LOG_PREFIX . " Processing $frequency reports");
$reportId = $this->option('report-id');
if ($reportId !== null) {
$reports = $this->getReportById($reportId);
} else {
// Get all enabled, not deleted reports with active teams for the specified frequency
$reports = $this->reportRepository->getActiveReportsByFrequency($frequency);
}
$this->logger->info(self::LOG_PREFIX . " Found {$reports->count()} $frequency reports to process");
/** @var AutomatedReport $report */
foreach ($reports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'frequency' => $report->getFrequency(),
'type' => $report->getType(),
]);
$job = $report->isAskJiminnyReport()
? new RequestGenerateAskJiminnyReportJob($report->getUuid())
: new RequestGenerateReportJob($report->getUuid());
$this->dispatcher->dispatch($job);
}
}
private function getReportById(string $reportId): Collection
{
$report = $this->reportRepository->findByIdOrUuid($reportId);
if ($report === null) {
$this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);
$this->warn("Report not found: {$reportId}");
return collect();
}
if (! $report->getStatus()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
]);
$this->warn('Report is inactive — processing anyway (manual override).');
}
$team = $report->getTeam();
if ($team->getStatus() !== Team::STATUS_ACTIVE) {
$this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'teamStatus' => $team->getStatus(),
]);
$this->warn("Team #{$report->getTeamId()} is not active — processing anyway (manual override).");
}
if ($report->isExpired()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()
. ') — processing anyway (manual override).');
}
$this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());
return collect([$report]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
76729
|
|
76708
|
1923
|
27
|
2026-04-24T08:17:43.522137+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777018663522_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Support\Collection;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Team;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
class AutomatedReportsCommand extends Command
{
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[automated-reports]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports
{--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).
Use --report-id to manually trigger a specific report by ID or UUID.';
public function __construct(
private readonly LoggerInterface $logger,
private readonly BusDispatcher $dispatcher,
private readonly AutomatedReportsRepository $reportRepository
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->logger->info(self::LOG_PREFIX . ' Started');
$this->disableExpiredAskJiminnyReports();
$now = Carbon::now();
$isMonday = $now->isMonday();
$isFirstDayOfMonth = $now->day === 1;
$currentMonth = $now->month;
// Check if the current month is a quarterly month (January, April, July, October)
$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);
$this->logger->info(self::LOG_PREFIX . ' Checking conditions', [
'isMonday' => $isMonday,
'isFirstDayOfMonth' => $isFirstDayOfMonth,
'currentMonth' => $currentMonth,
'isQuarterlyMonth' => $isQuarterlyMonth,
]);
// Process daily reports
$this->processReports(AutomatedReportsService::FREQUENCY_DAILY);
// Process weekly reports on Mondays
if ($isMonday) {
$this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);
}
// Process monthly reports on the first day of the month
if ($isFirstDayOfMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);
}
// Process quarterly reports on the first day of January, April, July, and October
if ($isFirstDayOfMonth && $isQuarterlyMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);
}
$this->logger->info(self::LOG_PREFIX . ' Completed');
return 0;
}
private function disableExpiredAskJiminnyReports(): void
{
$expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();
foreach ($expiredReports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->reportRepository->update($report, ['status' => false]);
}
}
/**
* Process reports for a specific frequency.
*
* @param string $frequency
*
* @return void
*/
private function processReports(string $frequency): void
{
$this->logger->info(self::LOG_PREFIX . " Processing $frequency reports");
$reportId = $this->option('report-id');
if ($reportId !== null) {
$reports = $this->getReportById($reportId);
} else {
// Get all enabled, not deleted reports with active teams for the specified frequency
$reports = $this->reportRepository->getActiveReportsByFrequency($frequency);
}
$this->logger->info(self::LOG_PREFIX . " Found {$reports->count()} $frequency reports to process");
/** @var AutomatedReport $report */
foreach ($reports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'frequency' => $report->getFrequency(),
'type' => $report->getType(),
]);
$job = $report->isAskJiminnyReport()
? new RequestGenerateAskJiminnyReportJob($report->getUuid())
: new RequestGenerateReportJob($report->getUuid());
$this->dispatcher->dispatch($job);
}
}
private function getReportById(string $reportId): Collection
{
$report = $this->reportRepository->findByIdOrUuid($reportId);
if ($report === null) {
$this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);
$this->warn("Report not found: {$reportId}");
return collect();
}
if (! $report->getStatus()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
]);
$this->warn('Report is inactive — processing anyway (manual override).');
}
$team = $report->getTeam();
if ($team->getStatus() !== Team::STATUS_ACTIVE) {
$this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'teamStatus' => $team->getStatus(),
]);
$this->warn("Team #{$report->getTeamId()} is not active — processing anyway (manual override).");
}
if ($report->isExpired()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()
. ') — processing anyway (manual override).');
}
$this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());
return collect([$report]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20738-debug-AJ-tracking-UP, menu","depth":5,"help_text":"Git Branch: JY-20738-debug-AJ-tracking-UP","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports\n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).\n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $this->disableExpiredAskJiminnyReports();\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n private function disableExpiredAskJiminnyReports(): void\n {\n $expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();\n\n foreach ($expiredReports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n\n $this->reportRepository->update($report, ['status' => false]);\n }\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()\n . ') — processing anyway (manual override).');\n }\n\n $this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());\n\n return collect([$report]);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports\n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).\n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $this->disableExpiredAskJiminnyReports();\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n private function disableExpiredAskJiminnyReports(): void\n {\n $expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();\n\n foreach ($expiredReports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n\n $this->reportRepository->update($report, ['status' => false]);\n }\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()\n . ') — processing anyway (manual override).');\n }\n\n $this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());\n\n return collect([$report]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4512205705669076910
|
8399920998164036400
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Support\Collection;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Team;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
class AutomatedReportsCommand extends Command
{
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[automated-reports]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports
{--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).
Use --report-id to manually trigger a specific report by ID or UUID.';
public function __construct(
private readonly LoggerInterface $logger,
private readonly BusDispatcher $dispatcher,
private readonly AutomatedReportsRepository $reportRepository
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->logger->info(self::LOG_PREFIX . ' Started');
$this->disableExpiredAskJiminnyReports();
$now = Carbon::now();
$isMonday = $now->isMonday();
$isFirstDayOfMonth = $now->day === 1;
$currentMonth = $now->month;
// Check if the current month is a quarterly month (January, April, July, October)
$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);
$this->logger->info(self::LOG_PREFIX . ' Checking conditions', [
'isMonday' => $isMonday,
'isFirstDayOfMonth' => $isFirstDayOfMonth,
'currentMonth' => $currentMonth,
'isQuarterlyMonth' => $isQuarterlyMonth,
]);
// Process daily reports
$this->processReports(AutomatedReportsService::FREQUENCY_DAILY);
// Process weekly reports on Mondays
if ($isMonday) {
$this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);
}
// Process monthly reports on the first day of the month
if ($isFirstDayOfMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);
}
// Process quarterly reports on the first day of January, April, July, and October
if ($isFirstDayOfMonth && $isQuarterlyMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);
}
$this->logger->info(self::LOG_PREFIX . ' Completed');
return 0;
}
private function disableExpiredAskJiminnyReports(): void
{
$expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();
foreach ($expiredReports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->reportRepository->update($report, ['status' => false]);
}
}
/**
* Process reports for a specific frequency.
*
* @param string $frequency
*
* @return void
*/
private function processReports(string $frequency): void
{
$this->logger->info(self::LOG_PREFIX . " Processing $frequency reports");
$reportId = $this->option('report-id');
if ($reportId !== null) {
$reports = $this->getReportById($reportId);
} else {
// Get all enabled, not deleted reports with active teams for the specified frequency
$reports = $this->reportRepository->getActiveReportsByFrequency($frequency);
}
$this->logger->info(self::LOG_PREFIX . " Found {$reports->count()} $frequency reports to process");
/** @var AutomatedReport $report */
foreach ($reports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'frequency' => $report->getFrequency(),
'type' => $report->getType(),
]);
$job = $report->isAskJiminnyReport()
? new RequestGenerateAskJiminnyReportJob($report->getUuid())
: new RequestGenerateReportJob($report->getUuid());
$this->dispatcher->dispatch($job);
}
}
private function getReportById(string $reportId): Collection
{
$report = $this->reportRepository->findByIdOrUuid($reportId);
if ($report === null) {
$this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);
$this->warn("Report not found: {$reportId}");
return collect();
}
if (! $report->getStatus()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
]);
$this->warn('Report is inactive — processing anyway (manual override).');
}
$team = $report->getTeam();
if ($team->getStatus() !== Team::STATUS_ACTIVE) {
$this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'teamStatus' => $team->getStatus(),
]);
$this->warn("Team #{$report->getTeamId()} is not active — processing anyway (manual override).");
}
if ($report->isExpired()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()
. ') — processing anyway (manual override).');
}
$this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());
return collect([$report]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
76709
|
1924
|
24
|
2026-04-24T08:17:43.425874+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777018663425_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Support\Collection;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Team;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
class AutomatedReportsCommand extends Command
{
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[automated-reports]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports
{--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).
Use --report-id to manually trigger a specific report by ID or UUID.';
public function __construct(
private readonly LoggerInterface $logger,
private readonly BusDispatcher $dispatcher,
private readonly AutomatedReportsRepository $reportRepository
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->logger->info(self::LOG_PREFIX . ' Started');
$this->disableExpiredAskJiminnyReports();
$now = Carbon::now();
$isMonday = $now->isMonday();
$isFirstDayOfMonth = $now->day === 1;
$currentMonth = $now->month;
// Check if the current month is a quarterly month (January, April, July, October)
$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);
$this->logger->info(self::LOG_PREFIX . ' Checking conditions', [
'isMonday' => $isMonday,
'isFirstDayOfMonth' => $isFirstDayOfMonth,
'currentMonth' => $currentMonth,
'isQuarterlyMonth' => $isQuarterlyMonth,
]);
// Process daily reports
$this->processReports(AutomatedReportsService::FREQUENCY_DAILY);
// Process weekly reports on Mondays
if ($isMonday) {
$this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);
}
// Process monthly reports on the first day of the month
if ($isFirstDayOfMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);
}
// Process quarterly reports on the first day of January, April, July, and October
if ($isFirstDayOfMonth && $isQuarterlyMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);
}
$this->logger->info(self::LOG_PREFIX . ' Completed');
return 0;
}
private function disableExpiredAskJiminnyReports(): void
{
$expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();
foreach ($expiredReports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->reportRepository->update($report, ['status' => false]);
}
}
/**
* Process reports for a specific frequency.
*
* @param string $frequency
*
* @return void
*/
private function processReports(string $frequency): void
{
$this->logger->info(self::LOG_PREFIX . " Processing $frequency reports");
$reportId = $this->option('report-id');
if ($reportId !== null) {
$reports = $this->getReportById($reportId);
} else {
// Get all enabled, not deleted reports with active teams for the specified frequency
$reports = $this->reportRepository->getActiveReportsByFrequency($frequency);
}
$this->logger->info(self::LOG_PREFIX . " Found {$reports->count()} $frequency reports to process");
/** @var AutomatedReport $report */
foreach ($reports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'frequency' => $report->getFrequency(),
'type' => $report->getType(),
]);
$job = $report->isAskJiminnyReport()
? new RequestGenerateAskJiminnyReportJob($report->getUuid())
: new RequestGenerateReportJob($report->getUuid());
$this->dispatcher->dispatch($job);
}
}
private function getReportById(string $reportId): Collection
{
$report = $this->reportRepository->findByIdOrUuid($reportId);
if ($report === null) {
$this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);
$this->warn("Report not found: {$reportId}");
return collect();
}
if (! $report->getStatus()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
]);
$this->warn('Report is inactive — processing anyway (manual override).');
}
$team = $report->getTeam();
if ($team->getStatus() !== Team::STATUS_ACTIVE) {
$this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'teamStatus' => $team->getStatus(),
]);
$this->warn("Team #{$report->getTeamId()} is not active — processing anyway (manual override).");
}
if ($report->isExpired()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()
. ') — processing anyway (manual override).');
}
$this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());
return collect([$report]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20738-debug-AJ-tracking-UP, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.08510638,"height":0.025538707},"help_text":"Git Branch: JY-20738-debug-AJ-tracking-UP","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.62799203,"top":0.10055866,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.63796544,"top":0.10055866,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6476064,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.6549202,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports\n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).\n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $this->disableExpiredAskJiminnyReports();\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n private function disableExpiredAskJiminnyReports(): void\n {\n $expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();\n\n foreach ($expiredReports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n\n $this->reportRepository->update($report, ['status' => false]);\n }\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()\n . ') — processing anyway (manual override).');\n }\n\n $this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());\n\n return collect([$report]);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports\n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).\n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $this->disableExpiredAskJiminnyReports();\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n private function disableExpiredAskJiminnyReports(): void\n {\n $expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();\n\n foreach ($expiredReports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n\n $this->reportRepository->update($report, ['status' => false]);\n }\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()\n . ') — processing anyway (manual override).');\n }\n\n $this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());\n\n return collect([$report]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4512205705669076910
|
8399920998164036400
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Support\Collection;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Team;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
class AutomatedReportsCommand extends Command
{
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[automated-reports]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports
{--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).
Use --report-id to manually trigger a specific report by ID or UUID.';
public function __construct(
private readonly LoggerInterface $logger,
private readonly BusDispatcher $dispatcher,
private readonly AutomatedReportsRepository $reportRepository
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->logger->info(self::LOG_PREFIX . ' Started');
$this->disableExpiredAskJiminnyReports();
$now = Carbon::now();
$isMonday = $now->isMonday();
$isFirstDayOfMonth = $now->day === 1;
$currentMonth = $now->month;
// Check if the current month is a quarterly month (January, April, July, October)
$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);
$this->logger->info(self::LOG_PREFIX . ' Checking conditions', [
'isMonday' => $isMonday,
'isFirstDayOfMonth' => $isFirstDayOfMonth,
'currentMonth' => $currentMonth,
'isQuarterlyMonth' => $isQuarterlyMonth,
]);
// Process daily reports
$this->processReports(AutomatedReportsService::FREQUENCY_DAILY);
// Process weekly reports on Mondays
if ($isMonday) {
$this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);
}
// Process monthly reports on the first day of the month
if ($isFirstDayOfMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);
}
// Process quarterly reports on the first day of January, April, July, and October
if ($isFirstDayOfMonth && $isQuarterlyMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);
}
$this->logger->info(self::LOG_PREFIX . ' Completed');
return 0;
}
private function disableExpiredAskJiminnyReports(): void
{
$expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();
foreach ($expiredReports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->reportRepository->update($report, ['status' => false]);
}
}
/**
* Process reports for a specific frequency.
*
* @param string $frequency
*
* @return void
*/
private function processReports(string $frequency): void
{
$this->logger->info(self::LOG_PREFIX . " Processing $frequency reports");
$reportId = $this->option('report-id');
if ($reportId !== null) {
$reports = $this->getReportById($reportId);
} else {
// Get all enabled, not deleted reports with active teams for the specified frequency
$reports = $this->reportRepository->getActiveReportsByFrequency($frequency);
}
$this->logger->info(self::LOG_PREFIX . " Found {$reports->count()} $frequency reports to process");
/** @var AutomatedReport $report */
foreach ($reports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'frequency' => $report->getFrequency(),
'type' => $report->getType(),
]);
$job = $report->isAskJiminnyReport()
? new RequestGenerateAskJiminnyReportJob($report->getUuid())
: new RequestGenerateReportJob($report->getUuid());
$this->dispatcher->dispatch($job);
}
}
private function getReportById(string $reportId): Collection
{
$report = $this->reportRepository->findByIdOrUuid($reportId);
if ($report === null) {
$this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);
$this->warn("Report not found: {$reportId}");
return collect();
}
if (! $report->getStatus()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
]);
$this->warn('Report is inactive — processing anyway (manual override).');
}
$team = $report->getTeam();
if ($team->getStatus() !== Team::STATUS_ACTIVE) {
$this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'teamStatus' => $team->getStatus(),
]);
$this->warn("Team #{$report->getTeamId()} is not active — processing anyway (manual override).");
}
if ($report->isExpired()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()
. ') — processing anyway (manual override).');
}
$this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());
return collect([$report]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
76707
|
|
76716
|
1923
|
31
|
2026-04-24T08:18:24.671508+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777018704671_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Support\Collection;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Team;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
class AutomatedReportsCommand extends Command
{
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[automated-reports]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports
{--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).
Use --report-id to manually trigger a specific report by ID or UUID.';
public function __construct(
private readonly LoggerInterface $logger,
private readonly BusDispatcher $dispatcher,
private readonly AutomatedReportsRepository $reportRepository
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->logger->info(self::LOG_PREFIX . ' Started');
$this->disableExpiredAskJiminnyReports();
$now = Carbon::now();
$isMonday = $now->isMonday();
$isFirstDayOfMonth = $now->day === 1;
$currentMonth = $now->month;
// Check if the current month is a quarterly month (January, April, July, October)
$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);
$this->logger->info(self::LOG_PREFIX . ' Checking conditions', [
'isMonday' => $isMonday,
'isFirstDayOfMonth' => $isFirstDayOfMonth,
'currentMonth' => $currentMonth,
'isQuarterlyMonth' => $isQuarterlyMonth,
]);
// Process daily reports
$this->processReports(AutomatedReportsService::FREQUENCY_DAILY);
// Process weekly reports on Mondays
if ($isMonday) {
$this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);
}
// Process monthly reports on the first day of the month
if ($isFirstDayOfMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);
}
// Process quarterly reports on the first day of January, April, July, and October
if ($isFirstDayOfMonth && $isQuarterlyMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);
}
$this->logger->info(self::LOG_PREFIX . ' Completed');
return 0;
}
private function disableExpiredAskJiminnyReports(): void
{
$expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();
foreach ($expiredReports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->reportRepository->update($report, ['status' => false]);
}
}
/**
* Process reports for a specific frequency.
*
* @param string $frequency
*
* @return void
*/
private function processReports(string $frequency): void
{
$this->logger->info(self::LOG_PREFIX . " Processing $frequency reports");
$reportId = $this->option('report-id');
if ($reportId !== null) {
$reports = $this->getReportById($reportId);
} else {
// Get all enabled, not deleted reports with active teams for the specified frequency
$reports = $this->reportRepository->getActiveReportsByFrequency($frequency);
}
$this->logger->info(self::LOG_PREFIX . " Found {$reports->count()} $frequency reports to process");
/** @var AutomatedReport $report */
foreach ($reports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'frequency' => $report->getFrequency(),
'type' => $report->getType(),
]);
$job = $report->isAskJiminnyReport()
? new RequestGenerateAskJiminnyReportJob($report->getUuid())
: new RequestGenerateReportJob($report->getUuid());
$this->dispatcher->dispatch($job);
}
}
private function getReportById(string $reportId): Collection
{
$report = $this->reportRepository->findByIdOrUuid($reportId);
if ($report === null) {
$this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);
$this->warn("Report not found: {$reportId}");
return collect();
}
if (! $report->getStatus()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
]);
$this->warn('Report is inactive — processing anyway (manual override).');
}
$team = $report->getTeam();
if ($team->getStatus() !== Team::STATUS_ACTIVE) {
$this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'teamStatus' => $team->getStatus(),
]);
$this->warn("Team #{$report->getTeamId()} is not active — processing anyway (manual override).");
}
if ($report->isExpired()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()
. ') — processing anyway (manual override).');
}
$this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());
return collect([$report]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20738-debug-AJ-tracking-UP, menu","depth":5,"help_text":"Git Branch: JY-20738-debug-AJ-tracking-UP","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports\n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).\n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $this->disableExpiredAskJiminnyReports();\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n private function disableExpiredAskJiminnyReports(): void\n {\n $expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();\n\n foreach ($expiredReports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n\n $this->reportRepository->update($report, ['status' => false]);\n }\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()\n . ') — processing anyway (manual override).');\n }\n\n $this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());\n\n return collect([$report]);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports\n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).\n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $this->disableExpiredAskJiminnyReports();\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n private function disableExpiredAskJiminnyReports(): void\n {\n $expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();\n\n foreach ($expiredReports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n\n $this->reportRepository->update($report, ['status' => false]);\n }\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()\n . ') — processing anyway (manual override).');\n }\n\n $this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());\n\n return collect([$report]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4512205705669076910
|
8399920998164036400
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Support\Collection;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Team;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
class AutomatedReportsCommand extends Command
{
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[automated-reports]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports
{--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).
Use --report-id to manually trigger a specific report by ID or UUID.';
public function __construct(
private readonly LoggerInterface $logger,
private readonly BusDispatcher $dispatcher,
private readonly AutomatedReportsRepository $reportRepository
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->logger->info(self::LOG_PREFIX . ' Started');
$this->disableExpiredAskJiminnyReports();
$now = Carbon::now();
$isMonday = $now->isMonday();
$isFirstDayOfMonth = $now->day === 1;
$currentMonth = $now->month;
// Check if the current month is a quarterly month (January, April, July, October)
$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);
$this->logger->info(self::LOG_PREFIX . ' Checking conditions', [
'isMonday' => $isMonday,
'isFirstDayOfMonth' => $isFirstDayOfMonth,
'currentMonth' => $currentMonth,
'isQuarterlyMonth' => $isQuarterlyMonth,
]);
// Process daily reports
$this->processReports(AutomatedReportsService::FREQUENCY_DAILY);
// Process weekly reports on Mondays
if ($isMonday) {
$this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);
}
// Process monthly reports on the first day of the month
if ($isFirstDayOfMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);
}
// Process quarterly reports on the first day of January, April, July, and October
if ($isFirstDayOfMonth && $isQuarterlyMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);
}
$this->logger->info(self::LOG_PREFIX . ' Completed');
return 0;
}
private function disableExpiredAskJiminnyReports(): void
{
$expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();
foreach ($expiredReports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->reportRepository->update($report, ['status' => false]);
}
}
/**
* Process reports for a specific frequency.
*
* @param string $frequency
*
* @return void
*/
private function processReports(string $frequency): void
{
$this->logger->info(self::LOG_PREFIX . " Processing $frequency reports");
$reportId = $this->option('report-id');
if ($reportId !== null) {
$reports = $this->getReportById($reportId);
} else {
// Get all enabled, not deleted reports with active teams for the specified frequency
$reports = $this->reportRepository->getActiveReportsByFrequency($frequency);
}
$this->logger->info(self::LOG_PREFIX . " Found {$reports->count()} $frequency reports to process");
/** @var AutomatedReport $report */
foreach ($reports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'frequency' => $report->getFrequency(),
'type' => $report->getType(),
]);
$job = $report->isAskJiminnyReport()
? new RequestGenerateAskJiminnyReportJob($report->getUuid())
: new RequestGenerateReportJob($report->getUuid());
$this->dispatcher->dispatch($job);
}
}
private function getReportById(string $reportId): Collection
{
$report = $this->reportRepository->findByIdOrUuid($reportId);
if ($report === null) {
$this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);
$this->warn("Report not found: {$reportId}");
return collect();
}
if (! $report->getStatus()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
]);
$this->warn('Report is inactive — processing anyway (manual override).');
}
$team = $report->getTeam();
if ($team->getStatus() !== Team::STATUS_ACTIVE) {
$this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'teamStatus' => $team->getStatus(),
]);
$this->warn("Team #{$report->getTeamId()} is not active — processing anyway (manual override).");
}
if ($report->isExpired()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()
. ') — processing anyway (manual override).');
}
$this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());
return collect([$report]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
76717
|
1924
|
28
|
2026-04-24T08:18:24.606316+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-24/1777 /Users/lukas/.screenpipe/data/data/2026-04-24/1777018704606_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Support\Collection;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Team;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
class AutomatedReportsCommand extends Command
{
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[automated-reports]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports
{--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).
Use --report-id to manually trigger a specific report by ID or UUID.';
public function __construct(
private readonly LoggerInterface $logger,
private readonly BusDispatcher $dispatcher,
private readonly AutomatedReportsRepository $reportRepository
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->logger->info(self::LOG_PREFIX . ' Started');
$this->disableExpiredAskJiminnyReports();
$now = Carbon::now();
$isMonday = $now->isMonday();
$isFirstDayOfMonth = $now->day === 1;
$currentMonth = $now->month;
// Check if the current month is a quarterly month (January, April, July, October)
$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);
$this->logger->info(self::LOG_PREFIX . ' Checking conditions', [
'isMonday' => $isMonday,
'isFirstDayOfMonth' => $isFirstDayOfMonth,
'currentMonth' => $currentMonth,
'isQuarterlyMonth' => $isQuarterlyMonth,
]);
// Process daily reports
$this->processReports(AutomatedReportsService::FREQUENCY_DAILY);
// Process weekly reports on Mondays
if ($isMonday) {
$this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);
}
// Process monthly reports on the first day of the month
if ($isFirstDayOfMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);
}
// Process quarterly reports on the first day of January, April, July, and October
if ($isFirstDayOfMonth && $isQuarterlyMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);
}
$this->logger->info(self::LOG_PREFIX . ' Completed');
return 0;
}
private function disableExpiredAskJiminnyReports(): void
{
$expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();
foreach ($expiredReports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->reportRepository->update($report, ['status' => false]);
}
}
/**
* Process reports for a specific frequency.
*
* @param string $frequency
*
* @return void
*/
private function processReports(string $frequency): void
{
$this->logger->info(self::LOG_PREFIX . " Processing $frequency reports");
$reportId = $this->option('report-id');
if ($reportId !== null) {
$reports = $this->getReportById($reportId);
} else {
// Get all enabled, not deleted reports with active teams for the specified frequency
$reports = $this->reportRepository->getActiveReportsByFrequency($frequency);
}
$this->logger->info(self::LOG_PREFIX . " Found {$reports->count()} $frequency reports to process");
/** @var AutomatedReport $report */
foreach ($reports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'frequency' => $report->getFrequency(),
'type' => $report->getType(),
]);
$job = $report->isAskJiminnyReport()
? new RequestGenerateAskJiminnyReportJob($report->getUuid())
: new RequestGenerateReportJob($report->getUuid());
$this->dispatcher->dispatch($job);
}
}
private function getReportById(string $reportId): Collection
{
$report = $this->reportRepository->findByIdOrUuid($reportId);
if ($report === null) {
$this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);
$this->warn("Report not found: {$reportId}");
return collect();
}
if (! $report->getStatus()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
]);
$this->warn('Report is inactive — processing anyway (manual override).');
}
$team = $report->getTeam();
if ($team->getStatus() !== Team::STATUS_ACTIVE) {
$this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'teamStatus' => $team->getStatus(),
]);
$this->warn("Team #{$report->getTeamId()} is not active — processing anyway (manual override).");
}
if ($report->isExpired()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()
. ') — processing anyway (manual override).');
}
$this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());
return collect([$report]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.25731382,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20738-debug-AJ-tracking-UP, menu","depth":5,"bounds":{"left":0.29587767,"top":0.019952115,"width":0.08510638,"height":0.025538707},"help_text":"Git Branch: JY-20738-debug-AJ-tracking-UP","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.62799203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.63796544,"top":0.12529927,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6476064,"top":0.123703115,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.6549202,"top":0.123703115,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports\n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).\n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $this->disableExpiredAskJiminnyReports();\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n private function disableExpiredAskJiminnyReports(): void\n {\n $expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();\n\n foreach ($expiredReports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n\n $this->reportRepository->update($report, ['status' => false]);\n }\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()\n . ') — processing anyway (manual override).');\n }\n\n $this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());\n\n return collect([$report]);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports\n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).\n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $this->disableExpiredAskJiminnyReports();\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n private function disableExpiredAskJiminnyReports(): void\n {\n $expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();\n\n foreach ($expiredReports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n\n $this->reportRepository->update($report, ['status' => false]);\n }\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()\n . ') — processing anyway (manual override).');\n }\n\n $this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());\n\n return collect([$report]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"43","depth":4,"bounds":{"left":0.96210104,"top":0.10055866,"width":0.010305851,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Contracts\\UserContract;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass TrackAutomatedReportGeneratedEvent implements ShouldQueue\n{\n use InteractsWithQueue;\n\n private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';\n private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';\n\n public string $queue = Constants::QUEUE_DELAYABLE;\n\n public function __construct(\n private readonly UserPilotClient $userPilotClient,\n private readonly AutomatedReportsService $automatedReportsService,\n ) {\n }\n\n public function handle(AutomatedReportGenerated $event): void\n {\n if (config('services.userpilot.token') === null) {\n return;\n }\n\n $automatedReport = $event->automatedReport;\n $payload = $this->buildPayload($automatedReport);\n\n $eventName = $this->resolveEventName($automatedReport);\n\n $users = $this->resolveUsers($automatedReport);\n\n if (empty($users)) {\n Log::warning('[UserPilot] No recipients found for automated report', [\n 'report_id' => $automatedReport->getId(),\n 'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),\n ]);\n\n return;\n }\n\n Log::info('[UserPilot] Sending automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'event_name' => $eventName,\n 'recipient_count' => count($users),\n ]);\n\n try {\n foreach ($users as $user) {\n $this->userPilotClient->track($user, $eventName, $payload);\n }\n } catch (GuzzleException $e) {\n Log::error('[UserPilot] Failed to send automated report event', [\n 'report_id' => $automatedReport->getId(),\n 'error' => $e->getMessage(),\n ]);\n $this->release(3600);\n }\n }\n\n /**\n * @return array<UserContract>\n */\n private function resolveUsers(AutomatedReport $automatedReport): array\n {\n if ($automatedReport->isAskJiminnyReport()) {\n $creator = $automatedReport->getCreator();\n\n return $creator !== null ? [$creator] : [];\n }\n\n return $this->automatedReportsService->getRecipientUserObjects($automatedReport);\n }\n\n private function buildPayload(AutomatedReport $automatedReport): array\n {\n return [\n 'report_type' => $automatedReport->getType(),\n 'frequency' => $automatedReport->getFrequency(),\n ];\n }\n\n private function resolveEventName(AutomatedReport $automatedReport): string\n {\n if ($automatedReport->isAskJiminnyReport()) {\n return self::EVENT_NAME_ASK_JIMINNY_REPORT;\n }\n\n return self::EVENT_NAME_AUTOMATED_REPORT;\n }\n}\n\n+++\n\n<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Listeners\\AutomatedReports\\UserPilot;\n\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Jiminny\\Events\\AutomatedReports\\AutomatedReportGenerated;\nuse Jiminny\\Listeners\\AutomatedReports\\UserPilot\\TrackAutomatedReportGeneratedEvent;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\UserPilot\\UserPilotClient;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\n\nclass TrackAutomatedReportGeneratedEventTest extends TestCase\n{\n private UserPilotClient&MockObject $userPilotClient;\n private AutomatedReportsService&MockObject $automatedReportsService;\n\n protected function setUp(): void\n {\n parent::setUp();\n $this->userPilotClient = $this->createMock(UserPilotClient::class);\n $this->automatedReportsService = $this->createMock(AutomatedReportsService::class);\n }\n\n private function makeListener(): TrackAutomatedReportGeneratedEvent\n {\n return new TrackAutomatedReportGeneratedEvent(\n $this->userPilotClient,\n $this->automatedReportsService,\n );\n }\n\n private function makeEvent(AutomatedReport $report): AutomatedReportGenerated\n {\n return new AutomatedReportGenerated($report);\n }\n\n public function testHandleSkipsWhenUserPilotTokenIsNull(): void\n {\n config(['services.userpilot.token' => null]);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->never())->method('isAskJiminnyReport');\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksCreatorForAskJiminnyReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn($creator);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(123);\n\n $this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $creator,\n 'ask-jiminny-report-generated',\n ['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);\n $report->expects($this->once())->method('getCreator')->willReturn(null);\n $report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->expects($this->once())->method('getFrequency')->willReturn('weekly');\n $report->expects($this->once())->method('getId')->willReturn(456);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleTracksAllRecipientsForExecReport(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $userOne = $this->createMock(User::class);\n $userTwo = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(789);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$userOne, $userTwo]);\n\n $this->userPilotClient->expects($this->exactly(2))\n ->method('track')\n ->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {\n $this->assertTrue($user === $userOne || $user === $userTwo);\n $this->assertSame('automated-report-generated', $eventName);\n $this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);\n\n return null;\n });\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('exec_summary');\n $report->expects($this->once())->method('getFrequency')->willReturn('monthly');\n $report->expects($this->once())->method('getId')->willReturn(101);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->willReturn([]);\n\n $this->userPilotClient->expects($this->never())->method('track');\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n\n public function testHandleDoesNotThrowOnGuzzleException(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $creator = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->method('isAskJiminnyReport')->willReturn(true);\n $report->method('getCreator')->willReturn($creator);\n $report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);\n $report->method('getFrequency')->willReturn('daily');\n $report->method('getId')->willReturn(202);\n\n $guzzleException = $this->createMock(GuzzleException::class);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with($creator, 'ask-jiminny-report-generated', $this->anything())\n ->willThrowException($guzzleException);\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n\n $this->addToAssertionCount(1);\n }\n\n public function testHandleTracksAutomatedReportWithSingleRecipient(): void\n {\n config(['services.userpilot.token' => 'NX-token']);\n\n $user = $this->createMock(User::class);\n\n $report = $this->createMock(AutomatedReport::class);\n $report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);\n $report->expects($this->once())->method('getType')->willReturn('team_performance');\n $report->expects($this->once())->method('getFrequency')->willReturn('daily');\n $report->expects($this->once())->method('getId')->willReturn(303);\n\n $this->automatedReportsService->expects($this->once())\n ->method('getRecipientUserObjects')\n ->with($report)\n ->willReturn([$user]);\n\n $this->userPilotClient->expects($this->once())\n ->method('track')\n ->with(\n $user,\n 'automated-report-generated',\n ['report_type' => 'team_performance', 'frequency' => 'daily']\n );\n\n $listener = $this->makeListener();\n $listener->handle($this->makeEvent($report));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.24335106,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4512205705669076910
|
8399920998164036400
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20738-debug-AJ-trackin Project: faVsco.js, menu
JY-20738-debug-AJ-tracking-UP, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Support\Collection;
use Jiminny\Jobs\AutomatedReports\RequestGenerateAskJiminnyReportJob;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Team;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
class AutomatedReportsCommand extends Command
{
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[automated-reports]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports
{--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly).
Use --report-id to manually trigger a specific report by ID or UUID.';
public function __construct(
private readonly LoggerInterface $logger,
private readonly BusDispatcher $dispatcher,
private readonly AutomatedReportsRepository $reportRepository
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->logger->info(self::LOG_PREFIX . ' Started');
$this->disableExpiredAskJiminnyReports();
$now = Carbon::now();
$isMonday = $now->isMonday();
$isFirstDayOfMonth = $now->day === 1;
$currentMonth = $now->month;
// Check if the current month is a quarterly month (January, April, July, October)
$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);
$this->logger->info(self::LOG_PREFIX . ' Checking conditions', [
'isMonday' => $isMonday,
'isFirstDayOfMonth' => $isFirstDayOfMonth,
'currentMonth' => $currentMonth,
'isQuarterlyMonth' => $isQuarterlyMonth,
]);
// Process daily reports
$this->processReports(AutomatedReportsService::FREQUENCY_DAILY);
// Process weekly reports on Mondays
if ($isMonday) {
$this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);
}
// Process monthly reports on the first day of the month
if ($isFirstDayOfMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);
}
// Process quarterly reports on the first day of January, April, July, and October
if ($isFirstDayOfMonth && $isQuarterlyMonth) {
$this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);
}
$this->logger->info(self::LOG_PREFIX . ' Completed');
return 0;
}
private function disableExpiredAskJiminnyReports(): void
{
$expiredReports = $this->reportRepository->getExpiredActiveAskJiminnyReports();
foreach ($expiredReports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Disabling expired Ask Jiminny report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->reportRepository->update($report, ['status' => false]);
}
}
/**
* Process reports for a specific frequency.
*
* @param string $frequency
*
* @return void
*/
private function processReports(string $frequency): void
{
$this->logger->info(self::LOG_PREFIX . " Processing $frequency reports");
$reportId = $this->option('report-id');
if ($reportId !== null) {
$reports = $this->getReportById($reportId);
} else {
// Get all enabled, not deleted reports with active teams for the specified frequency
$reports = $this->reportRepository->getActiveReportsByFrequency($frequency);
}
$this->logger->info(self::LOG_PREFIX . " Found {$reports->count()} $frequency reports to process");
/** @var AutomatedReport $report */
foreach ($reports as $report) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'frequency' => $report->getFrequency(),
'type' => $report->getType(),
]);
$job = $report->isAskJiminnyReport()
? new RequestGenerateAskJiminnyReportJob($report->getUuid())
: new RequestGenerateReportJob($report->getUuid());
$this->dispatcher->dispatch($job);
}
}
private function getReportById(string $reportId): Collection
{
$report = $this->reportRepository->findByIdOrUuid($reportId);
if ($report === null) {
$this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);
$this->warn("Report not found: {$reportId}");
return collect();
}
if (! $report->getStatus()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
]);
$this->warn('Report is inactive — processing anyway (manual override).');
}
$team = $report->getTeam();
if ($team->getStatus() !== Team::STATUS_ACTIVE) {
$this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'teamId' => $report->getTeamId(),
'teamStatus' => $team->getStatus(),
]);
$this->warn("Team #{$report->getTeamId()} is not active — processing anyway (manual override).");
}
if ($report->isExpired()) {
$this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [
'reportId' => $reportId,
'reportUuid' => $report->getUuid(),
'expiresAt' => $report->getExpiresAt()?->toDateString(),
]);
$this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString()
. ') — processing anyway (manual override).');
}
$this->info(self::LOG_PREFIX . ' Automated report found ' . $report->getCustomName());
return collect([$report]);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
43
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use Illuminate\Support\Facades\Log;
class TrackAutomatedReportGeneratedEvent implements ShouldQueue
{
use InteractsWithQueue;
private const string EVENT_NAME_AUTOMATED_REPORT = 'automated-report-generated';
private const string EVENT_NAME_ASK_JIMINNY_REPORT = 'ask-jiminny-report-generated';
public string $queue = Constants::QUEUE_DELAYABLE;
public function __construct(
private readonly UserPilotClient $userPilotClient,
private readonly AutomatedReportsService $automatedReportsService,
) {
}
public function handle(AutomatedReportGenerated $event): void
{
if (config('services.userpilot.token') === null) {
return;
}
$automatedReport = $event->automatedReport;
$payload = $this->buildPayload($automatedReport);
$eventName = $this->resolveEventName($automatedReport);
$users = $this->resolveUsers($automatedReport);
if (empty($users)) {
Log::warning('[UserPilot] No recipients found for automated report', [
'report_id' => $automatedReport->getId(),
'is_ask_jiminny' => $automatedReport->isAskJiminnyReport(),
]);
return;
}
Log::info('[UserPilot] Sending automated report event', [
'report_id' => $automatedReport->getId(),
'event_name' => $eventName,
'recipient_count' => count($users),
]);
try {
foreach ($users as $user) {
$this->userPilotClient->track($user, $eventName, $payload);
}
} catch (GuzzleException $e) {
Log::error('[UserPilot] Failed to send automated report event', [
'report_id' => $automatedReport->getId(),
'error' => $e->getMessage(),
]);
$this->release(3600);
}
}
/**
* @return array<UserContract>
*/
private function resolveUsers(AutomatedReport $automatedReport): array
{
if ($automatedReport->isAskJiminnyReport()) {
$creator = $automatedReport->getCreator();
return $creator !== null ? [$creator] : [];
}
return $this->automatedReportsService->getRecipientUserObjects($automatedReport);
}
private function buildPayload(AutomatedReport $automatedReport): array
{
return [
'report_type' => $automatedReport->getType(),
'frequency' => $automatedReport->getFrequency(),
];
}
private function resolveEventName(AutomatedReport $automatedReport): string
{
if ($automatedReport->isAskJiminnyReport()) {
return self::EVENT_NAME_ASK_JIMINNY_REPORT;
}
return self::EVENT_NAME_AUTOMATED_REPORT;
}
}
+++
<?php
declare(strict_types=1);
namespace Tests\Unit\Listeners\AutomatedReports\UserPilot;
use GuzzleHttp\Exception\GuzzleException;
use Jiminny\Events\AutomatedReports\AutomatedReportGenerated;
use Jiminny\Listeners\AutomatedReports\UserPilot\TrackAutomatedReportGeneratedEvent;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\UserPilot\UserPilotClient;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
class TrackAutomatedReportGeneratedEventTest extends TestCase
{
private UserPilotClient&MockObject $userPilotClient;
private AutomatedReportsService&MockObject $automatedReportsService;
protected function setUp(): void
{
parent::setUp();
$this->userPilotClient = $this->createMock(UserPilotClient::class);
$this->automatedReportsService = $this->createMock(AutomatedReportsService::class);
}
private function makeListener(): TrackAutomatedReportGeneratedEvent
{
return new TrackAutomatedReportGeneratedEvent(
$this->userPilotClient,
$this->automatedReportsService,
);
}
private function makeEvent(AutomatedReport $report): AutomatedReportGenerated
{
return new AutomatedReportGenerated($report);
}
public function testHandleSkipsWhenUserPilotTokenIsNull(): void
{
config(['services.userpilot.token' => null]);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->never())->method('isAskJiminnyReport');
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksCreatorForAskJiminnyReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn($creator);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(123);
$this->automatedReportsService->expects($this->never())->method('getRecipientUserObjects');
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$creator,
'ask-jiminny-report-generated',
['report_type' => AutomatedReportsService::TYPE_ASK_JIMINNY, 'frequency' => 'weekly']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleSkipsTrackingWhenAskJiminnyCreatorIsNull(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(true);
$report->expects($this->once())->method('getCreator')->willReturn(null);
$report->expects($this->once())->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->expects($this->once())->method('getFrequency')->willReturn('weekly');
$report->expects($this->once())->method('getId')->willReturn(456);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleTracksAllRecipientsForExecReport(): void
{
config(['services.userpilot.token' => 'NX-token']);
$userOne = $this->createMock(User::class);
$userTwo = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(789);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$userOne, $userTwo]);
$this->userPilotClient->expects($this->exactly(2))
->method('track')
->willReturnCallback(function ($user, $eventName, $payload) use ($userOne, $userTwo) {
$this->assertTrue($user === $userOne || $user === $userTwo);
$this->assertSame('automated-report-generated', $eventName);
$this->assertSame(['report_type' => 'exec_summary', 'frequency' => 'monthly'], $payload);
return null;
});
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotTrackWhenExecReportHasNoRecipients(): void
{
config(['services.userpilot.token' => 'NX-token']);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(3))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('exec_summary');
$report->expects($this->once())->method('getFrequency')->willReturn('monthly');
$report->expects($this->once())->method('getId')->willReturn(101);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->willReturn([]);
$this->userPilotClient->expects($this->never())->method('track');
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
public function testHandleDoesNotThrowOnGuzzleException(): void
{
config(['services.userpilot.token' => 'NX-token']);
$creator = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->method('isAskJiminnyReport')->willReturn(true);
$report->method('getCreator')->willReturn($creator);
$report->method('getType')->willReturn(AutomatedReportsService::TYPE_ASK_JIMINNY);
$report->method('getFrequency')->willReturn('daily');
$report->method('getId')->willReturn(202);
$guzzleException = $this->createMock(GuzzleException::class);
$this->userPilotClient->expects($this->once())
->method('track')
->with($creator, 'ask-jiminny-report-generated', $this->anything())
->willThrowException($guzzleException);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
$this->addToAssertionCount(1);
}
public function testHandleTracksAutomatedReportWithSingleRecipient(): void
{
config(['services.userpilot.token' => 'NX-token']);
$user = $this->createMock(User::class);
$report = $this->createMock(AutomatedReport::class);
$report->expects($this->exactly(2))->method('isAskJiminnyReport')->willReturn(false);
$report->expects($this->once())->method('getType')->willReturn('team_performance');
$report->expects($this->once())->method('getFrequency')->willReturn('daily');
$report->expects($this->once())->method('getId')->willReturn(303);
$this->automatedReportsService->expects($this->once())
->method('getRecipientUserObjects')
->with($report)
->willReturn([$user]);
$this->userPilotClient->expects($this->once())
->method('track')
->with(
$user,
'automated-report-generated',
['report_type' => 'team_performance', 'frequency' => 'daily']
);
$listener = $this->makeListener();
$listener->handle($this->makeEvent($report));
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
76715
|
|
53781
|
1162
|
29
|
2026-04-20T08:28:03.941457+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-20/1776 /Users/lukas/.screenpipe/data/data/2026-04-20/1776673683941_m1.jpg...
|
PhpStorm
|
faVsco.js – RouteServiceProvider.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
18
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Providers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
use Jiminny\Component\DealRisks\DealRisk;
use Jiminny\Exceptions\LogicException;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Controllers\CustomerApi\CustomerApiController;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Ai\AiScorecard;
use Jiminny\Models\Ai\AiScorecardRule;
use Jiminny\Models\Ai\CrmTemplate;
use Jiminny\Models\Ai\CrmTemplateField;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\Group;
use Jiminny\Models\Invitation;
use Jiminny\Models\JobTitle;
use Jiminny\Models\Nudge;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Models\Session;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
class RouteServiceProvider extends ServiceProvider
{
/**
* Define your route model bindings, pattern filters, etc.
*/
public function boot(): void
{
$this->configureRateLimiting();
$this->routes(function (Router $router) {
$this
->mapHealthRoutes($router)
->mapWebhookRoutes($router)
->mapApiRoutes($router)
->mapApiV2Routes($router)
->mapWebRoutes($router)
->mapScimRoutes($router)
->mapCustomerApiRoutes($router)
->mapEmbeddedRoutes($router)
;
});
$this->defineRouteBindings();
}
private function mapHealthRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
], static function (Router $router) {
require base_path('routes/health.php');
});
return $this;
}
/**
* Define the route model bindings.
*/
protected function defineRouteBindings()
{
Route::pattern('teamSlug', '^[.\-_a-z0-9]*$');
Route::pattern('userSlug', '^[.\-_a-z0-9]*$');
Route::bind('participant', function ($value) {
return Participant::uuid($value);
});
Route::bind('participantA', static fn (string $value): Participant => Participant::uuid($value));
Route::bind('participantB', static fn (string $value): Participant => Participant::uuid($value));
Route::bind('connection', function ($value) {
return Connection::uuid($value);
});
Route::bind('activity', function ($value) {
return Activity::uuid($value);
});
Route::bind('session', function (string $value) {
return Session::uuid($value);
});
Route::bind('transcription', function ($value) {
return Activity\Transcription::uuid($value);
});
Route::bind('transcriptionProvider', static function (string $scorecardUuid): Models\TranscriptionProvider {
return Models\TranscriptionProvider::where(
'uuid',
Models\TranscriptionProvider::toOptimized($scorecardUuid)
)->firstOrFail();
});
Route::bind('coachingFeedback', function ($value) {
return CoachingFeedback::uuid($value);
});
Route::bind('team', function ($value) {
return Team::uuid($value);
});
Route::bind('crmTemplate', fn ($value) => CrmTemplate::uuid($value));
Route::bind('crmTemplateField', fn ($value) => CrmTemplateField::uuid($value));
Route::bind('aiScorecard', fn ($value) => AiScorecard::uuid($value));
Route::bind('aiScorecardRule', fn ($value) => AiScorecardRule::uuid($value));
Route::bind('group', function ($value) {
return Group::uuid($value);
});
Route::bind('user', function ($value) {
return User::uuid($value);
});
Route::bind('comment', function (string $value, \Illuminate\Routing\Route $route) {
$targetController = $route->getController();
if (! $targetController instanceof CommentContextInterface) {
throw new LogicException(
'Type hinting comment will require additional effort due to polymorphism.'
. ' Either implement CommentContextInterface or inject $commentId and process manually',
);
}
$commentClass = $targetController::getCommentImplementation();
return $commentClass::uuid($value);
});
Route::bind('track', function ($value) {
return Track::uuid($value);
});
Route::bind('invitation', function ($value) {
return Invitation::uuid($value);
});
Route::bind('playbook', function ($value) {
return Playbook::uuid($value);
});
Route::bind('category', function ($value) {
return PlaybookCategory::uuid($value);
});
Route::bind('coachingSection', function ($value) {
return CoachingSection::uuid($value);
});
Route::bind('coachingSectionCriterion', function ($value) {
return Models\CoachingSectionCriterion::uuid($value);
});
Route::bind('playlist', function ($value) {
return Playlist::uuid($value);
});
Route::bind('playlistActivity', static fn (string $value) => PlaylistActivity::uuid($value));
Route::bind('playlistTrack', static fn (string $value) => PlaylistActivity::uuid($value));
Route::bind('playlistShare', static fn (string $uuid) => Playlist\Share::uuid($uuid));
Route::bind('job', function ($value) {
return JobTitle::uuid($value);
});
Route::bind('notification', function ($value) {
return Activity\AvailabilityNotification::uuid($value);
});
Route::bind('activityMoment', function ($value) {
return Activity\Moment::uuid($value);
});
Route::bind('search', function ($value) {
return Activity\Search::uuid($value);
});
Route::bind('nudge', static function ($value): Nudge {
return Nudge::uuid($value);
});
Route::bind('theme', static function ($value): Models\PlaybackTheme {
return Models\PlaybackTheme::uuid($value);
});
Route::bind('topic', static function ($value): Models\PlaybackTheme\Topic {
return Models\PlaybackTheme\Topic::uuid($value);
});
Route::bind('topicTrigger', static function ($value): Models\PlaybackTheme\TopicTrigger {
return Models\PlaybackTheme\TopicTrigger::uuid($value);
});
Route::bind('vocabulary', static function (string $id): Models\Vocabulary {
return Models\Vocabulary::uuid($id);
});
Route::bind('teamDomain', function ($value) {
return Models\TeamDomain::uuid($value);
});
Route::bind('opportunity', function ($value) {
return Opportunity::uuid($value);
});
Route::bind('dealRisk', function ($value) {
return DealRisk::uuid($value);
});
Route::bind('layout', static fn (string $uuid) => Models\Crm\Layout::uuid($uuid));
Route::bind('scorecard', static function (string $scorecardUuid): Models\Scorecard\Scorecard {
return Models\Scorecard\Scorecard::where(
'uuid',
Models\Scorecard\Scorecard::toOptimized($scorecardUuid)
)->firstOrFail();
});
Route::bind('scorecardRule', static function (string $scorecardRuleUuid): Models\Scorecard\ScorecardRule {
return Models\Scorecard\ScorecardRule::where(
'uuid',
Models\Scorecard\ScorecardRule::toOptimized($scorecardRuleUuid)
)->firstOrFail();
});
Route::bind('askAnythingPrompt', function ($value) {
return Models\AskAnything\AskAnythingPrompt::uuid($value);
});
}
/**
* Define the routes for the application.
*
* @param \Illuminate\Routing\Router $router
*/
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*/
private function mapWebRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['web'],
], static function (Router $router) {
require base_path('routes/web.php');
});
return $this;
}
/**
* Define the "Embedded" routes for the application.
*
* These routes depend on a Partitioned cookie,
* and are accessible only after login.
*/
private function mapEmbeddedRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['embedded'],
'prefix' => 'embedded',
], static function (Router $router) {
require base_path('routes/embedded.php');
});
return $this;
}
private function mapWebhookRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['webhook'],
'prefix' => 'webhook',
], function (Router $router) {
require base_path('routes/webhook.php');
});
return $this;
}
/**
* Define the "api" routes for the application.
*/
private function mapApiRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['api'],
'prefix' => 'api/v1',
], function (Router $router) {
require base_path('routes/api.php');
});
return $this;
}
private function mapApiV2Routes(Router $router): self
{
$router->group([
'middleware' => ['api', 'auth:api'],
'prefix' => 'api/v2',
], static function (Router $router) {
require base_path('routes/api_v2.php');
});
return $this;
}
private function mapScimRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['scim'],
'prefix' => 'scim/v2',
], function (Router $router) {
require base_path('routes/scim.php');
});
return $this;
}
private function mapCustomerApiRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['customer-api'],
'prefix' => 'customer/api/v1',
], function (Router $router) {
require base_path('routes/customer_api.php');
});
return $this;
}
/** Configure the rate limiters for the application. */
protected function configureRateLimiting(): void
{
RateLimiter::for('api', static function (Request $request) {
$user = $request->user();
if ($user instanceof User || $user instanceof Models\Partner) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
if (! is_string($subject)) {
throw new LogicException('Unable to identify subject to rate limit');
}
return Limit::perMinute(120)->by($subject);
});
RateLimiter::for('customer-api', static function (Request $request) {
// Get the token using the same method as the middleware
$token = null;
if ($request->bearerToken() !== null) {
$token = $request->bearerToken();
} elseif ($request->get('api_token') !== null) {
$token = $request->get('api_token');
} elseif ($request->post('api_token') !== null) {
$token = $request->post('api_token');
}
// If we have a valid token, get the team ID for rate limiting
if ($token !== null && strlen($token) === Team::CUSTOMER_API_TOKEN_LENGTH) {
$tokens = Cache::remember(Team::CUSTOMER_API_TOKENS_CACHE, 60 * 60, static function () {
return Team::query()->whereNotNull('api_token')->pluck('id', 'api_token');
});
$hashedToken = hash('sha256', $token);
if (is_countable($tokens) && ! empty($tokens) && isset($tokens[$hashedToken])) {
$teamId = $tokens[$hashedToken];
$team = Team::find($teamId);
if ($team) {
$subject = $team->getUuid();
// Special rate limit for Funding Circle and Cision.
if (in_array($subject, [
'9762611a-fc86-4234-baba-a436de26e774', '1151b5b0-0680-4190-b30c-72649280837d',
'16b50903-a115-448f-877c-6c01e8b45f5d', 'bfc1c304-83b2-47a3-a7ce-1d28b0e1e90e',
'447ee473-7dd8-41bc-a702-9322e5b0ec5b'])) {
// Route-specific rate limiting
if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {
return Limit::perMinute(40)->by($subject);
}
return Limit::perMinute(1000)->by($subject);
}
// Route-specific rate limiting
if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {
return Limit::perMinute(30)->by($subject);
}
return Limit::perMinute(120)->by($subject);
}
}
}
// Default fallback to IP-based limiting
return Limit::perMinute(120)->by($request->ip());
});
RateLimiter::for('conference-consent', static function (Request $request) {
// Rate limit by IP address for public consent endpoint
// Allow 10 requests per minute per IP to prevent abuse while allowing legitimate use
return Limit::perMinute(10)->by($request->ip());
});
RateLimiter::for('activity-export-shareable-link', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(30)->by($subject);
});
RateLimiter::for('activity-export', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(10)->by($subject);
});
RateLimiter::for('transcription-download', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(3)->by($subject);
});
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
14
Previous Highlighted Error
Next Highlighted Error
{
"name": "jiminny/app",
"description": "The Jiminny Platform.",
"keywords": [
"training",
"salesforce",
"conference"
],
"license": "MIT",
"type": "project",
"require": {
"php": "^8.3",
"ext-ctype": "*",
"ext-curl": "*",
"ext-date": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-filter": "*",
"ext-gd": "*",
"ext-gmp": "*",
"ext-hash": "*",
"ext-iconv": "*",
"ext-igbinary": "*",
"ext-imagick": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mailparse": "*",
"ext-mbstring": "*",
"ext-mysqlnd": "*",
"ext-openssl": "*",
"ext-pcntl": "*",
"ext-pcre": "*",
"ext-pdo": "*",
"ext-pdo_mysql": "*",
"ext-phar": "*",
"ext-phpiredis": "*",
"ext-posix": "*",
"ext-readline": "*",
"ext-redis": "*",
"ext-reflection": "*",
"ext-session": "*",
"ext-simplexml": "*",
"ext-sockets": "*",
"ext-spl": "*",
"ext-tokenizer": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zend-opcache": "*",
"ext-zip": "*",
"ext-zlib": "*",
"lib-curl": "*",
"lib-curl-openssl": "*",
"lib-curl-zlib": "*",
"lib-date-timelib": "*",
"lib-date-zoneinfo": "*",
"lib-fileinfo-libmagic": "*",
"lib-gd": "*",
"lib-gd-freetype": "*",
"lib-gd-libjpeg": "*",
"lib-gd-libpng": "*",
"lib-gmp": "*",
"lib-icu": "*",
"lib-icu-cldr": "*",
"lib-icu-unicode": "*",
"lib-imagick-imagemagick": "*",
"lib-libxml": "*",
"lib-mbstring-libmbfl": "*",
"lib-mbstring-oniguruma": "*",
"lib-openssl": "*",
"lib-pcre": "*",
"lib-pcre-unicode": "*",
"lib-zip-libzip": "*",
"lib-zlib": "*",
"24slides/laravel-saml2": "^2.4",
"adam-paterson/oauth2-slack": "^1.1",
"asimlqt/php-google-spreadsheet-client": "^3.0",
"aws/aws-sdk-php": "^3.368",
"aws/aws-sdk-php-laravel": "^3.10",
"bepsvpt/secure-headers": "^9.0",
"chadhutchins/oauth2-slack": "^1.2",
"chaseconey/laravel-datadog-helper": "^1.2",
"chrisyue/php-m3u8": "4.0.3",
"daniti/oauth2-pipedrive": "dev-master",
"devio/pipedrive": "^2.6",
"doctrine/dbal": "^4.0",
"elasticsearch/elasticsearch": "^7.11",
"erusev/parsedown": "^1.7",
"fakerphp/faker": "^1.23",
"firebase/php-jwt": "^7.0",
"flipboxdigital/oauth2-hubspot": "1.0.1",
"giggsey/libphonenumber-for-php": "^8.12",
"google/apiclient": "^2.19",
"google/apiclient-services": "~0.360",
"google/apps-meet": "^0.5.1",
"guzzlehttp/guzzle": "^7.8",
"guzzlehttp/psr7": "^2.6",
"halaxa/json-machine": "^1.2",
"html2text/html2text": "^4.3",
"hubspot/api-client": "~5.0.0",
"hubspot/hubspot-php": "^5.2.0",
"intercom/intercom-php": "^4.5",
"intervention/image": "^3.4",
"jakeasmith/http_build_url": "^1.0",
"jdavidbakr/cloudfront-proxies": "^1.7",
"jeremykendall/php-domain-parser": "^6.3",
"jiminny/oauth2-aircall": "dev-master",
"jiminny/oauth2-bullhorn": "^0.2.0",
"jiminny/oauth2-dialpad": "dev-master",
"jiminny/oauth2-salesloft": "dev-master",
"jolicode/slack-php-api": "^4.5.0",
"kalnoy/nestedset": "*",
"laravel/framework": "^12.28",
"laravel/helpers": "^1.7",
"laravel/passport": "^13.0",
"laravel/slack-notification-channel": "^3.4",
"laravel/tinker": "^2.10.1",
"laravel/ui": "^4.6",
"laravolt/avatar": "^6.1",
"league/flysystem": "^3.0",
"league/flysystem-aws-s3-v3": "^3.0",
"league/fractal": "*",
"league/oauth2-client": "^2.7",
"league/oauth2-google": "^4.0",
"league/oauth2-linkedin": "^5.1",
"league/oauth2-server": "^9.2",
"league/statsd": "^2.0",
"markrogoyski/math-php": "^2.7.0",
"microsoft/microsoft-graph": "^2.51",
"monolog/monolog": "^3.0",
"nesbot/carbon": "^3.8",
"nette/caching": "*",
"phlib/sms-length": "^2.0",
"php-ffmpeg/php-ffmpeg": "^1.2",
"php-http/client-common": "^2.7",
"php-http/curl-client": "^2.3",
"php-http/httplug": "^2.2",
"php-http/message": "^1.16",
"phpseclib/phpseclib": "^3.0.36",
"propaganistas/laravel-phone": "^5.3",
"psr/cache": "^3.0",
"psr/http-message": "^2.0",
"psr/log": "^3.0",
"psr/simple-cache": "^3.0",
"pusher/pusher-php-server": "7.2.3",
"ramsey/uuid": "^4.2",
"ringcentral/ringcentral-php": "3.0.0",
"rmccue/requests": "^2.0",
"ruflin/elastica": "^7.1.1",
"santigarcor/laratrust": "^8.4",
"sentry/sentry": "4.13.0",
"sentry/sentry-laravel": "~4.13.0",
"shiftonelabs/laravel-sqs-fifo-queue": "^3.0",
"spatie/fractalistic": "^2.9",
"spatie/laravel-fractal": "^6.3",
"spatie/laravel-ignition": "^2.9",
"spatie/laravel-webhook-server": "^3.8",
"staudenmeir/belongs-to-through": "^2.17",
"stevenmaguire/oauth2-salesforce": "^2.0",
"symfony/cache": "^7.2",
"symfony/console": "^7.2",
"symfony/css-selector": "^7.2",
"symfony/debug": "^4.4",
"symfony/dom-crawler": "^7.2",
"symfony/expression-language": "^7.2",
"symfony/finder": "^7.2",
"symfony/http-client": "^7.3",
"symfony/http-foundation": "^7.2",
"symfony/http-kernel": "^7.2",
"symfony/postmark-mailer": "^7.3",
"symfony/process": "^7.3",
"symfony/property-access": "^7.2",
"symfony/psr-http-message-bridge": "^7.0",
"symfony/var-dumper": "^7.2",
"symfony/workflow": "^7.2",
"tecnickcom/tcpdf": "^6.11",
"thenetworg/oauth2-azure": "dev-master",
"tmannherz/oauth2-ringcentral": "dev-master",
"twilio/sdk": "^8.3",
"vanderlee/php-sentence": "^1.0",
"vinkla/hashids": "^13.0",
"vlucas/phpdotenv": "^5.4",
"wildbit/postmark-php": "^6.0",
"willdurand/email-reply-parser": "^2.8",
"zbateson/mail-mime-parser": "^3.0.4"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.15",
"barryvdh/laravel-ide-helper": "^3.5",
"brianium/paratest": "^7.5",
"browserstack/browserstack-local": "^1.1.0",
"filp/whoops": "^2.9",
"friendsofphp/php-cs-fixer": "^3.66",
"infection/infection": "^0.29.14",
"jasonmccreary/laravel-test-assertions": "^2.5",
"larastan/larastan": "^3.1",
"maglnet/composer-require-checker": "^4.8",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^11.5.50",
"symfony/phpunit-bridge": "^7.0",
"vimeo/psalm": "^6.5.0"
},
"autoload": {
"classmap": [
"database"
],
"psr-4": {
"Jiminny\\": "app/",
"Tests\\": "tests/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/",
"Microsoft\\Graph\\Generated\\Models\\": "app/Services/MeetingGenerator/Overrides/Microsoft/Graph/Generated/Models/"
},
"files": [
"app/helpers.php"
]
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
],
"psr-4": {
"Jiminny\\": "app/",
"Tests\\": "tests/"
}
},
"scripts": {
"post-root-package-install": [
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"php artisan key:generate --ansi"
],
"post-install-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postInstall"
],
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan ide-helper:generate",
"php artisan ide-helper:meta",
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
]
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true,
"allow-plugins": {
"infection/extension-installer": true,
"php-http/discovery": true,
"tbachert/spi": true
}
},
"extra": {
"laravel": {
"dont-discover": [
"laravel/dusk"
]
},
"metasyntactical/composer-plugin-license-check": {
"whitelist": [],
"blacklist": [
"AGPL"
]
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/PHP-FFMpeg/BinaryDriver.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-salesloft.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-aircall.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-pipedrive.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-ringcentral"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-dialpad.git"
}
],
"prefer-stable": true
}
Install
Update
Show log
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"18","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Providers;\n\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\RateLimiter;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Routing\\Router;\nuse Illuminate\\Foundation\\Support\\Providers\\RouteServiceProvider as ServiceProvider;\nuse Illuminate\\Support\\Facades\\Route;\nuse Jiminny\\Component\\DealRisks\\DealRisk;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Controllers\\CustomerApi\\CustomerApiController;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Ai\\AiScorecard;\nuse Jiminny\\Models\\Ai\\AiScorecardRule;\nuse Jiminny\\Models\\Ai\\CrmTemplate;\nuse Jiminny\\Models\\Ai\\CrmTemplateField;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Invitation;\nuse Jiminny\\Models\\JobTitle;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Models\\Session;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\n\nclass RouteServiceProvider extends ServiceProvider\n{\n /**\n * Define your route model bindings, pattern filters, etc.\n */\n public function boot(): void\n {\n $this->configureRateLimiting();\n\n $this->routes(function (Router $router) {\n $this\n ->mapHealthRoutes($router)\n ->mapWebhookRoutes($router)\n ->mapApiRoutes($router)\n ->mapApiV2Routes($router)\n ->mapWebRoutes($router)\n ->mapScimRoutes($router)\n ->mapCustomerApiRoutes($router)\n ->mapEmbeddedRoutes($router)\n ;\n });\n $this->defineRouteBindings();\n }\n\n private function mapHealthRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n ], static function (Router $router) {\n require base_path('routes/health.php');\n });\n\n return $this;\n }\n\n /**\n * Define the route model bindings.\n */\n protected function defineRouteBindings()\n {\n Route::pattern('teamSlug', '^[.\\-_a-z0-9]*$');\n Route::pattern('userSlug', '^[.\\-_a-z0-9]*$');\n\n Route::bind('participant', function ($value) {\n return Participant::uuid($value);\n });\n\n Route::bind('participantA', static fn (string $value): Participant => Participant::uuid($value));\n\n Route::bind('participantB', static fn (string $value): Participant => Participant::uuid($value));\n\n Route::bind('connection', function ($value) {\n return Connection::uuid($value);\n });\n\n Route::bind('activity', function ($value) {\n return Activity::uuid($value);\n });\n\n Route::bind('session', function (string $value) {\n return Session::uuid($value);\n });\n\n Route::bind('transcription', function ($value) {\n return Activity\\Transcription::uuid($value);\n });\n\n Route::bind('transcriptionProvider', static function (string $scorecardUuid): Models\\TranscriptionProvider {\n return Models\\TranscriptionProvider::where(\n 'uuid',\n Models\\TranscriptionProvider::toOptimized($scorecardUuid)\n )->firstOrFail();\n });\n\n Route::bind('coachingFeedback', function ($value) {\n return CoachingFeedback::uuid($value);\n });\n\n Route::bind('team', function ($value) {\n return Team::uuid($value);\n });\n\n Route::bind('crmTemplate', fn ($value) => CrmTemplate::uuid($value));\n\n Route::bind('crmTemplateField', fn ($value) => CrmTemplateField::uuid($value));\n\n Route::bind('aiScorecard', fn ($value) => AiScorecard::uuid($value));\n\n Route::bind('aiScorecardRule', fn ($value) => AiScorecardRule::uuid($value));\n\n Route::bind('group', function ($value) {\n return Group::uuid($value);\n });\n\n Route::bind('user', function ($value) {\n return User::uuid($value);\n });\n\n Route::bind('comment', function (string $value, \\Illuminate\\Routing\\Route $route) {\n $targetController = $route->getController();\n if (! $targetController instanceof CommentContextInterface) {\n throw new LogicException(\n 'Type hinting comment will require additional effort due to polymorphism.'\n . ' Either implement CommentContextInterface or inject $commentId and process manually',\n );\n }\n\n $commentClass = $targetController::getCommentImplementation();\n\n return $commentClass::uuid($value);\n });\n\n Route::bind('track', function ($value) {\n return Track::uuid($value);\n });\n\n Route::bind('invitation', function ($value) {\n return Invitation::uuid($value);\n });\n\n Route::bind('playbook', function ($value) {\n return Playbook::uuid($value);\n });\n\n Route::bind('category', function ($value) {\n return PlaybookCategory::uuid($value);\n });\n\n Route::bind('coachingSection', function ($value) {\n return CoachingSection::uuid($value);\n });\n\n Route::bind('coachingSectionCriterion', function ($value) {\n return Models\\CoachingSectionCriterion::uuid($value);\n });\n\n Route::bind('playlist', function ($value) {\n return Playlist::uuid($value);\n });\n\n Route::bind('playlistActivity', static fn (string $value) => PlaylistActivity::uuid($value));\n\n Route::bind('playlistTrack', static fn (string $value) => PlaylistActivity::uuid($value));\n\n Route::bind('playlistShare', static fn (string $uuid) => Playlist\\Share::uuid($uuid));\n\n Route::bind('job', function ($value) {\n return JobTitle::uuid($value);\n });\n\n Route::bind('notification', function ($value) {\n return Activity\\AvailabilityNotification::uuid($value);\n });\n\n Route::bind('activityMoment', function ($value) {\n return Activity\\Moment::uuid($value);\n });\n\n Route::bind('search', function ($value) {\n return Activity\\Search::uuid($value);\n });\n\n Route::bind('nudge', static function ($value): Nudge {\n return Nudge::uuid($value);\n });\n\n Route::bind('theme', static function ($value): Models\\PlaybackTheme {\n return Models\\PlaybackTheme::uuid($value);\n });\n\n Route::bind('topic', static function ($value): Models\\PlaybackTheme\\Topic {\n return Models\\PlaybackTheme\\Topic::uuid($value);\n });\n\n Route::bind('topicTrigger', static function ($value): Models\\PlaybackTheme\\TopicTrigger {\n return Models\\PlaybackTheme\\TopicTrigger::uuid($value);\n });\n\n Route::bind('vocabulary', static function (string $id): Models\\Vocabulary {\n return Models\\Vocabulary::uuid($id);\n });\n\n Route::bind('teamDomain', function ($value) {\n return Models\\TeamDomain::uuid($value);\n });\n\n Route::bind('opportunity', function ($value) {\n return Opportunity::uuid($value);\n });\n\n Route::bind('dealRisk', function ($value) {\n return DealRisk::uuid($value);\n });\n\n Route::bind('layout', static fn (string $uuid) => Models\\Crm\\Layout::uuid($uuid));\n\n Route::bind('scorecard', static function (string $scorecardUuid): Models\\Scorecard\\Scorecard {\n return Models\\Scorecard\\Scorecard::where(\n 'uuid',\n Models\\Scorecard\\Scorecard::toOptimized($scorecardUuid)\n )->firstOrFail();\n });\n\n Route::bind('scorecardRule', static function (string $scorecardRuleUuid): Models\\Scorecard\\ScorecardRule {\n return Models\\Scorecard\\ScorecardRule::where(\n 'uuid',\n Models\\Scorecard\\ScorecardRule::toOptimized($scorecardRuleUuid)\n )->firstOrFail();\n });\n\n Route::bind('askAnythingPrompt', function ($value) {\n return Models\\AskAnything\\AskAnythingPrompt::uuid($value);\n });\n }\n\n /**\n * Define the routes for the application.\n *\n * @param \\Illuminate\\Routing\\Router $router\n */\n\n /**\n * Define the \"web\" routes for the application.\n *\n * These routes all receive session state, CSRF protection, etc.\n */\n private function mapWebRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['web'],\n ], static function (Router $router) {\n require base_path('routes/web.php');\n });\n\n return $this;\n }\n\n /**\n * Define the \"Embedded\" routes for the application.\n *\n * These routes depend on a Partitioned cookie,\n * and are accessible only after login.\n */\n private function mapEmbeddedRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['embedded'],\n 'prefix' => 'embedded',\n ], static function (Router $router) {\n require base_path('routes/embedded.php');\n });\n\n return $this;\n }\n\n private function mapWebhookRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['webhook'],\n 'prefix' => 'webhook',\n ], function (Router $router) {\n require base_path('routes/webhook.php');\n });\n\n return $this;\n }\n\n /**\n * Define the \"api\" routes for the application.\n */\n private function mapApiRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['api'],\n 'prefix' => 'api/v1',\n ], function (Router $router) {\n require base_path('routes/api.php');\n });\n\n return $this;\n }\n\n private function mapApiV2Routes(Router $router): self\n {\n $router->group([\n 'middleware' => ['api', 'auth:api'],\n 'prefix' => 'api/v2',\n ], static function (Router $router) {\n require base_path('routes/api_v2.php');\n });\n\n return $this;\n }\n\n private function mapScimRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['scim'],\n 'prefix' => 'scim/v2',\n ], function (Router $router) {\n require base_path('routes/scim.php');\n });\n\n return $this;\n }\n\n private function mapCustomerApiRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['customer-api'],\n 'prefix' => 'customer/api/v1',\n ], function (Router $router) {\n require base_path('routes/customer_api.php');\n });\n\n return $this;\n }\n\n /** Configure the rate limiters for the application. */\n protected function configureRateLimiting(): void\n {\n RateLimiter::for('api', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User || $user instanceof Models\\Partner) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n if (! is_string($subject)) {\n throw new LogicException('Unable to identify subject to rate limit');\n }\n\n return Limit::perMinute(120)->by($subject);\n });\n\n RateLimiter::for('customer-api', static function (Request $request) {\n // Get the token using the same method as the middleware\n $token = null;\n if ($request->bearerToken() !== null) {\n $token = $request->bearerToken();\n } elseif ($request->get('api_token') !== null) {\n $token = $request->get('api_token');\n } elseif ($request->post('api_token') !== null) {\n $token = $request->post('api_token');\n }\n\n // If we have a valid token, get the team ID for rate limiting\n if ($token !== null && strlen($token) === Team::CUSTOMER_API_TOKEN_LENGTH) {\n $tokens = Cache::remember(Team::CUSTOMER_API_TOKENS_CACHE, 60 * 60, static function () {\n return Team::query()->whereNotNull('api_token')->pluck('id', 'api_token');\n });\n\n $hashedToken = hash('sha256', $token);\n if (is_countable($tokens) && ! empty($tokens) && isset($tokens[$hashedToken])) {\n $teamId = $tokens[$hashedToken];\n $team = Team::find($teamId);\n\n if ($team) {\n $subject = $team->getUuid();\n\n // Special rate limit for Funding Circle and Cision.\n if (in_array($subject, [\n '9762611a-fc86-4234-baba-a436de26e774', '1151b5b0-0680-4190-b30c-72649280837d',\n '16b50903-a115-448f-877c-6c01e8b45f5d', 'bfc1c304-83b2-47a3-a7ce-1d28b0e1e90e',\n '447ee473-7dd8-41bc-a702-9322e5b0ec5b'])) {\n // Route-specific rate limiting\n if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {\n return Limit::perMinute(40)->by($subject);\n }\n\n return Limit::perMinute(1000)->by($subject);\n }\n\n // Route-specific rate limiting\n if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {\n return Limit::perMinute(30)->by($subject);\n }\n\n return Limit::perMinute(120)->by($subject);\n }\n }\n }\n\n // Default fallback to IP-based limiting\n return Limit::perMinute(120)->by($request->ip());\n });\n\n RateLimiter::for('conference-consent', static function (Request $request) {\n // Rate limit by IP address for public consent endpoint\n // Allow 10 requests per minute per IP to prevent abuse while allowing legitimate use\n return Limit::perMinute(10)->by($request->ip());\n });\n\n RateLimiter::for('activity-export-shareable-link', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(30)->by($subject);\n });\n\n RateLimiter::for('activity-export', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(10)->by($subject);\n });\n\n RateLimiter::for('transcription-download', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(3)->by($subject);\n });\n }\n}","depth":4,"value":"<?php\n\nnamespace Jiminny\\Providers;\n\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\RateLimiter;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Routing\\Router;\nuse Illuminate\\Foundation\\Support\\Providers\\RouteServiceProvider as ServiceProvider;\nuse Illuminate\\Support\\Facades\\Route;\nuse Jiminny\\Component\\DealRisks\\DealRisk;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Controllers\\CustomerApi\\CustomerApiController;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Ai\\AiScorecard;\nuse Jiminny\\Models\\Ai\\AiScorecardRule;\nuse Jiminny\\Models\\Ai\\CrmTemplate;\nuse Jiminny\\Models\\Ai\\CrmTemplateField;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Invitation;\nuse Jiminny\\Models\\JobTitle;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Models\\Session;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\n\nclass RouteServiceProvider extends ServiceProvider\n{\n /**\n * Define your route model bindings, pattern filters, etc.\n */\n public function boot(): void\n {\n $this->configureRateLimiting();\n\n $this->routes(function (Router $router) {\n $this\n ->mapHealthRoutes($router)\n ->mapWebhookRoutes($router)\n ->mapApiRoutes($router)\n ->mapApiV2Routes($router)\n ->mapWebRoutes($router)\n ->mapScimRoutes($router)\n ->mapCustomerApiRoutes($router)\n ->mapEmbeddedRoutes($router)\n ;\n });\n $this->defineRouteBindings();\n }\n\n private function mapHealthRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n ], static function (Router $router) {\n require base_path('routes/health.php');\n });\n\n return $this;\n }\n\n /**\n * Define the route model bindings.\n */\n protected function defineRouteBindings()\n {\n Route::pattern('teamSlug', '^[.\\-_a-z0-9]*$');\n Route::pattern('userSlug', '^[.\\-_a-z0-9]*$');\n\n Route::bind('participant', function ($value) {\n return Participant::uuid($value);\n });\n\n Route::bind('participantA', static fn (string $value): Participant => Participant::uuid($value));\n\n Route::bind('participantB', static fn (string $value): Participant => Participant::uuid($value));\n\n Route::bind('connection', function ($value) {\n return Connection::uuid($value);\n });\n\n Route::bind('activity', function ($value) {\n return Activity::uuid($value);\n });\n\n Route::bind('session', function (string $value) {\n return Session::uuid($value);\n });\n\n Route::bind('transcription', function ($value) {\n return Activity\\Transcription::uuid($value);\n });\n\n Route::bind('transcriptionProvider', static function (string $scorecardUuid): Models\\TranscriptionProvider {\n return Models\\TranscriptionProvider::where(\n 'uuid',\n Models\\TranscriptionProvider::toOptimized($scorecardUuid)\n )->firstOrFail();\n });\n\n Route::bind('coachingFeedback', function ($value) {\n return CoachingFeedback::uuid($value);\n });\n\n Route::bind('team', function ($value) {\n return Team::uuid($value);\n });\n\n Route::bind('crmTemplate', fn ($value) => CrmTemplate::uuid($value));\n\n Route::bind('crmTemplateField', fn ($value) => CrmTemplateField::uuid($value));\n\n Route::bind('aiScorecard', fn ($value) => AiScorecard::uuid($value));\n\n Route::bind('aiScorecardRule', fn ($value) => AiScorecardRule::uuid($value));\n\n Route::bind('group', function ($value) {\n return Group::uuid($value);\n });\n\n Route::bind('user', function ($value) {\n return User::uuid($value);\n });\n\n Route::bind('comment', function (string $value, \\Illuminate\\Routing\\Route $route) {\n $targetController = $route->getController();\n if (! $targetController instanceof CommentContextInterface) {\n throw new LogicException(\n 'Type hinting comment will require additional effort due to polymorphism.'\n . ' Either implement CommentContextInterface or inject $commentId and process manually',\n );\n }\n\n $commentClass = $targetController::getCommentImplementation();\n\n return $commentClass::uuid($value);\n });\n\n Route::bind('track', function ($value) {\n return Track::uuid($value);\n });\n\n Route::bind('invitation', function ($value) {\n return Invitation::uuid($value);\n });\n\n Route::bind('playbook', function ($value) {\n return Playbook::uuid($value);\n });\n\n Route::bind('category', function ($value) {\n return PlaybookCategory::uuid($value);\n });\n\n Route::bind('coachingSection', function ($value) {\n return CoachingSection::uuid($value);\n });\n\n Route::bind('coachingSectionCriterion', function ($value) {\n return Models\\CoachingSectionCriterion::uuid($value);\n });\n\n Route::bind('playlist', function ($value) {\n return Playlist::uuid($value);\n });\n\n Route::bind('playlistActivity', static fn (string $value) => PlaylistActivity::uuid($value));\n\n Route::bind('playlistTrack', static fn (string $value) => PlaylistActivity::uuid($value));\n\n Route::bind('playlistShare', static fn (string $uuid) => Playlist\\Share::uuid($uuid));\n\n Route::bind('job', function ($value) {\n return JobTitle::uuid($value);\n });\n\n Route::bind('notification', function ($value) {\n return Activity\\AvailabilityNotification::uuid($value);\n });\n\n Route::bind('activityMoment', function ($value) {\n return Activity\\Moment::uuid($value);\n });\n\n Route::bind('search', function ($value) {\n return Activity\\Search::uuid($value);\n });\n\n Route::bind('nudge', static function ($value): Nudge {\n return Nudge::uuid($value);\n });\n\n Route::bind('theme', static function ($value): Models\\PlaybackTheme {\n return Models\\PlaybackTheme::uuid($value);\n });\n\n Route::bind('topic', static function ($value): Models\\PlaybackTheme\\Topic {\n return Models\\PlaybackTheme\\Topic::uuid($value);\n });\n\n Route::bind('topicTrigger', static function ($value): Models\\PlaybackTheme\\TopicTrigger {\n return Models\\PlaybackTheme\\TopicTrigger::uuid($value);\n });\n\n Route::bind('vocabulary', static function (string $id): Models\\Vocabulary {\n return Models\\Vocabulary::uuid($id);\n });\n\n Route::bind('teamDomain', function ($value) {\n return Models\\TeamDomain::uuid($value);\n });\n\n Route::bind('opportunity', function ($value) {\n return Opportunity::uuid($value);\n });\n\n Route::bind('dealRisk', function ($value) {\n return DealRisk::uuid($value);\n });\n\n Route::bind('layout', static fn (string $uuid) => Models\\Crm\\Layout::uuid($uuid));\n\n Route::bind('scorecard', static function (string $scorecardUuid): Models\\Scorecard\\Scorecard {\n return Models\\Scorecard\\Scorecard::where(\n 'uuid',\n Models\\Scorecard\\Scorecard::toOptimized($scorecardUuid)\n )->firstOrFail();\n });\n\n Route::bind('scorecardRule', static function (string $scorecardRuleUuid): Models\\Scorecard\\ScorecardRule {\n return Models\\Scorecard\\ScorecardRule::where(\n 'uuid',\n Models\\Scorecard\\ScorecardRule::toOptimized($scorecardRuleUuid)\n )->firstOrFail();\n });\n\n Route::bind('askAnythingPrompt', function ($value) {\n return Models\\AskAnything\\AskAnythingPrompt::uuid($value);\n });\n }\n\n /**\n * Define the routes for the application.\n *\n * @param \\Illuminate\\Routing\\Router $router\n */\n\n /**\n * Define the \"web\" routes for the application.\n *\n * These routes all receive session state, CSRF protection, etc.\n */\n private function mapWebRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['web'],\n ], static function (Router $router) {\n require base_path('routes/web.php');\n });\n\n return $this;\n }\n\n /**\n * Define the \"Embedded\" routes for the application.\n *\n * These routes depend on a Partitioned cookie,\n * and are accessible only after login.\n */\n private function mapEmbeddedRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['embedded'],\n 'prefix' => 'embedded',\n ], static function (Router $router) {\n require base_path('routes/embedded.php');\n });\n\n return $this;\n }\n\n private function mapWebhookRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['webhook'],\n 'prefix' => 'webhook',\n ], function (Router $router) {\n require base_path('routes/webhook.php');\n });\n\n return $this;\n }\n\n /**\n * Define the \"api\" routes for the application.\n */\n private function mapApiRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['api'],\n 'prefix' => 'api/v1',\n ], function (Router $router) {\n require base_path('routes/api.php');\n });\n\n return $this;\n }\n\n private function mapApiV2Routes(Router $router): self\n {\n $router->group([\n 'middleware' => ['api', 'auth:api'],\n 'prefix' => 'api/v2',\n ], static function (Router $router) {\n require base_path('routes/api_v2.php');\n });\n\n return $this;\n }\n\n private function mapScimRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['scim'],\n 'prefix' => 'scim/v2',\n ], function (Router $router) {\n require base_path('routes/scim.php');\n });\n\n return $this;\n }\n\n private function mapCustomerApiRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['customer-api'],\n 'prefix' => 'customer/api/v1',\n ], function (Router $router) {\n require base_path('routes/customer_api.php');\n });\n\n return $this;\n }\n\n /** Configure the rate limiters for the application. */\n protected function configureRateLimiting(): void\n {\n RateLimiter::for('api', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User || $user instanceof Models\\Partner) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n if (! is_string($subject)) {\n throw new LogicException('Unable to identify subject to rate limit');\n }\n\n return Limit::perMinute(120)->by($subject);\n });\n\n RateLimiter::for('customer-api', static function (Request $request) {\n // Get the token using the same method as the middleware\n $token = null;\n if ($request->bearerToken() !== null) {\n $token = $request->bearerToken();\n } elseif ($request->get('api_token') !== null) {\n $token = $request->get('api_token');\n } elseif ($request->post('api_token') !== null) {\n $token = $request->post('api_token');\n }\n\n // If we have a valid token, get the team ID for rate limiting\n if ($token !== null && strlen($token) === Team::CUSTOMER_API_TOKEN_LENGTH) {\n $tokens = Cache::remember(Team::CUSTOMER_API_TOKENS_CACHE, 60 * 60, static function () {\n return Team::query()->whereNotNull('api_token')->pluck('id', 'api_token');\n });\n\n $hashedToken = hash('sha256', $token);\n if (is_countable($tokens) && ! empty($tokens) && isset($tokens[$hashedToken])) {\n $teamId = $tokens[$hashedToken];\n $team = Team::find($teamId);\n\n if ($team) {\n $subject = $team->getUuid();\n\n // Special rate limit for Funding Circle and Cision.\n if (in_array($subject, [\n '9762611a-fc86-4234-baba-a436de26e774', '1151b5b0-0680-4190-b30c-72649280837d',\n '16b50903-a115-448f-877c-6c01e8b45f5d', 'bfc1c304-83b2-47a3-a7ce-1d28b0e1e90e',\n '447ee473-7dd8-41bc-a702-9322e5b0ec5b'])) {\n // Route-specific rate limiting\n if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {\n return Limit::perMinute(40)->by($subject);\n }\n\n return Limit::perMinute(1000)->by($subject);\n }\n\n // Route-specific rate limiting\n if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {\n return Limit::perMinute(30)->by($subject);\n }\n\n return Limit::perMinute(120)->by($subject);\n }\n }\n }\n\n // Default fallback to IP-based limiting\n return Limit::perMinute(120)->by($request->ip());\n });\n\n RateLimiter::for('conference-consent', static function (Request $request) {\n // Rate limit by IP address for public consent endpoint\n // Allow 10 requests per minute per IP to prevent abuse while allowing legitimate use\n return Limit::perMinute(10)->by($request->ip());\n });\n\n RateLimiter::for('activity-export-shareable-link', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(30)->by($subject);\n });\n\n RateLimiter::for('activity-export', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(10)->by($subject);\n });\n\n RateLimiter::for('transcription-download', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(3)->by($subject);\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"14","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"{\n \"name\": \"jiminny/app\",\n \"description\": \"The Jiminny Platform.\",\n \"keywords\": [\n \"training\",\n \"salesforce\",\n \"conference\"\n ],\n \"license\": \"MIT\",\n \"type\": \"project\",\n \"require\": {\n \"php\": \"^8.3\",\n \"ext-ctype\": \"*\",\n \"ext-curl\": \"*\",\n \"ext-date\": \"*\",\n \"ext-dom\": \"*\",\n \"ext-fileinfo\": \"*\",\n \"ext-filter\": \"*\",\n \"ext-gd\": \"*\",\n \"ext-gmp\": \"*\",\n \"ext-hash\": \"*\",\n \"ext-iconv\": \"*\",\n \"ext-igbinary\": \"*\",\n \"ext-imagick\": \"*\",\n \"ext-intl\": \"*\",\n \"ext-json\": \"*\",\n \"ext-libxml\": \"*\",\n \"ext-mailparse\": \"*\",\n \"ext-mbstring\": \"*\",\n \"ext-mysqlnd\": \"*\",\n \"ext-openssl\": \"*\",\n \"ext-pcntl\": \"*\",\n \"ext-pcre\": \"*\",\n \"ext-pdo\": \"*\",\n \"ext-pdo_mysql\": \"*\",\n \"ext-phar\": \"*\",\n \"ext-phpiredis\": \"*\",\n \"ext-posix\": \"*\",\n \"ext-readline\": \"*\",\n \"ext-redis\": \"*\",\n \"ext-reflection\": \"*\",\n \"ext-session\": \"*\",\n \"ext-simplexml\": \"*\",\n \"ext-sockets\": \"*\",\n \"ext-spl\": \"*\",\n \"ext-tokenizer\": \"*\",\n \"ext-xml\": \"*\",\n \"ext-xmlreader\": \"*\",\n \"ext-xmlwriter\": \"*\",\n \"ext-zend-opcache\": \"*\",\n \"ext-zip\": \"*\",\n \"ext-zlib\": \"*\",\n \"lib-curl\": \"*\",\n \"lib-curl-openssl\": \"*\",\n \"lib-curl-zlib\": \"*\",\n \"lib-date-timelib\": \"*\",\n \"lib-date-zoneinfo\": \"*\",\n \"lib-fileinfo-libmagic\": \"*\",\n \"lib-gd\": \"*\",\n \"lib-gd-freetype\": \"*\",\n \"lib-gd-libjpeg\": \"*\",\n \"lib-gd-libpng\": \"*\",\n \"lib-gmp\": \"*\",\n \"lib-icu\": \"*\",\n \"lib-icu-cldr\": \"*\",\n \"lib-icu-unicode\": \"*\",\n \"lib-imagick-imagemagick\": \"*\",\n \"lib-libxml\": \"*\",\n \"lib-mbstring-libmbfl\": \"*\",\n \"lib-mbstring-oniguruma\": \"*\",\n \"lib-openssl\": \"*\",\n \"lib-pcre\": \"*\",\n \"lib-pcre-unicode\": \"*\",\n \"lib-zip-libzip\": \"*\",\n \"lib-zlib\": \"*\",\n \"24slides/laravel-saml2\": \"^2.4\",\n \"adam-paterson/oauth2-slack\": \"^1.1\",\n \"asimlqt/php-google-spreadsheet-client\": \"^3.0\",\n \"aws/aws-sdk-php\": \"^3.368\",\n \"aws/aws-sdk-php-laravel\": \"^3.10\",\n \"bepsvpt/secure-headers\": \"^9.0\",\n \"chadhutchins/oauth2-slack\": \"^1.2\",\n \"chaseconey/laravel-datadog-helper\": \"^1.2\",\n \"chrisyue/php-m3u8\": \"4.0.3\",\n \"daniti/oauth2-pipedrive\": \"dev-master\",\n \"devio/pipedrive\": \"^2.6\",\n \"doctrine/dbal\": \"^4.0\",\n \"elasticsearch/elasticsearch\": \"^7.11\",\n \"erusev/parsedown\": \"^1.7\",\n \"fakerphp/faker\": \"^1.23\",\n \"firebase/php-jwt\": \"^7.0\",\n \"flipboxdigital/oauth2-hubspot\": \"1.0.1\",\n \"giggsey/libphonenumber-for-php\": \"^8.12\",\n \"google/apiclient\": \"^2.19\",\n \"google/apiclient-services\": \"~0.360\",\n \"google/apps-meet\": \"^0.5.1\",\n \"guzzlehttp/guzzle\": \"^7.8\",\n \"guzzlehttp/psr7\": \"^2.6\",\n \"halaxa/json-machine\": \"^1.2\",\n \"html2text/html2text\": \"^4.3\",\n \"hubspot/api-client\": \"~5.0.0\",\n \"hubspot/hubspot-php\": \"^5.2.0\",\n \"intercom/intercom-php\": \"^4.5\",\n \"intervention/image\": \"^3.4\",\n \"jakeasmith/http_build_url\": \"^1.0\",\n \"jdavidbakr/cloudfront-proxies\": \"^1.7\",\n \"jeremykendall/php-domain-parser\": \"^6.3\",\n \"jiminny/oauth2-aircall\": \"dev-master\",\n \"jiminny/oauth2-bullhorn\": \"^0.2.0\",\n \"jiminny/oauth2-dialpad\": \"dev-master\",\n \"jiminny/oauth2-salesloft\": \"dev-master\",\n \"jolicode/slack-php-api\": \"^4.5.0\",\n \"kalnoy/nestedset\": \"*\",\n \"laravel/framework\": \"^12.28\",\n \"laravel/helpers\": \"^1.7\",\n \"laravel/passport\": \"^13.0\",\n \"laravel/slack-notification-channel\": \"^3.4\",\n \"laravel/tinker\": \"^2.10.1\",\n \"laravel/ui\": \"^4.6\",\n \"laravolt/avatar\": \"^6.1\",\n \"league/flysystem\": \"^3.0\",\n \"league/flysystem-aws-s3-v3\": \"^3.0\",\n \"league/fractal\": \"*\",\n \"league/oauth2-client\": \"^2.7\",\n \"league/oauth2-google\": \"^4.0\",\n \"league/oauth2-linkedin\": \"^5.1\",\n \"league/oauth2-server\": \"^9.2\",\n \"league/statsd\": \"^2.0\",\n \"markrogoyski/math-php\": \"^2.7.0\",\n \"microsoft/microsoft-graph\": \"^2.51\",\n \"monolog/monolog\": \"^3.0\",\n \"nesbot/carbon\": \"^3.8\",\n \"nette/caching\": \"*\",\n \"phlib/sms-length\": \"^2.0\",\n \"php-ffmpeg/php-ffmpeg\": \"^1.2\",\n \"php-http/client-common\": \"^2.7\",\n \"php-http/curl-client\": \"^2.3\",\n \"php-http/httplug\": \"^2.2\",\n \"php-http/message\": \"^1.16\",\n \"phpseclib/phpseclib\": \"^3.0.36\",\n \"propaganistas/laravel-phone\": \"^5.3\",\n \"psr/cache\": \"^3.0\",\n \"psr/http-message\": \"^2.0\",\n \"psr/log\": \"^3.0\",\n \"psr/simple-cache\": \"^3.0\",\n \"pusher/pusher-php-server\": \"7.2.3\",\n \"ramsey/uuid\": \"^4.2\",\n \"ringcentral/ringcentral-php\": \"3.0.0\",\n \"rmccue/requests\": \"^2.0\",\n \"ruflin/elastica\": \"^7.1.1\",\n \"santigarcor/laratrust\": \"^8.4\",\n \"sentry/sentry\": \"4.13.0\",\n \"sentry/sentry-laravel\": \"~4.13.0\",\n \"shiftonelabs/laravel-sqs-fifo-queue\": \"^3.0\",\n \"spatie/fractalistic\": \"^2.9\",\n \"spatie/laravel-fractal\": \"^6.3\",\n \"spatie/laravel-ignition\": \"^2.9\",\n \"spatie/laravel-webhook-server\": \"^3.8\",\n \"staudenmeir/belongs-to-through\": \"^2.17\",\n \"stevenmaguire/oauth2-salesforce\": \"^2.0\",\n \"symfony/cache\": \"^7.2\",\n \"symfony/console\": \"^7.2\",\n \"symfony/css-selector\": \"^7.2\",\n \"symfony/debug\": \"^4.4\",\n \"symfony/dom-crawler\": \"^7.2\",\n \"symfony/expression-language\": \"^7.2\",\n \"symfony/finder\": \"^7.2\",\n \"symfony/http-client\": \"^7.3\",\n \"symfony/http-foundation\": \"^7.2\",\n \"symfony/http-kernel\": \"^7.2\",\n \"symfony/postmark-mailer\": \"^7.3\",\n \"symfony/process\": \"^7.3\",\n \"symfony/property-access\": \"^7.2\",\n \"symfony/psr-http-message-bridge\": \"^7.0\",\n \"symfony/var-dumper\": \"^7.2\",\n \"symfony/workflow\": \"^7.2\",\n \"tecnickcom/tcpdf\": \"^6.11\",\n \"thenetworg/oauth2-azure\": \"dev-master\",\n \"tmannherz/oauth2-ringcentral\": \"dev-master\",\n \"twilio/sdk\": \"^8.3\",\n \"vanderlee/php-sentence\": \"^1.0\",\n \"vinkla/hashids\": \"^13.0\",\n \"vlucas/phpdotenv\": \"^5.4\",\n \"wildbit/postmark-php\": \"^6.0\",\n \"willdurand/email-reply-parser\": \"^2.8\",\n \"zbateson/mail-mime-parser\": \"^3.0.4\"\n },\n \"require-dev\": {\n \"barryvdh/laravel-debugbar\": \"^3.15\",\n \"barryvdh/laravel-ide-helper\": \"^3.5\",\n \"brianium/paratest\": \"^7.5\",\n \"browserstack/browserstack-local\": \"^1.1.0\",\n \"filp/whoops\": \"^2.9\",\n \"friendsofphp/php-cs-fixer\": \"^3.66\",\n \"infection/infection\": \"^0.29.14\",\n \"jasonmccreary/laravel-test-assertions\": \"^2.5\",\n \"larastan/larastan\": \"^3.1\",\n \"maglnet/composer-require-checker\": \"^4.8\",\n \"mockery/mockery\": \"^1.6\",\n \"nunomaduro/collision\": \"^8.6\",\n \"phpstan/phpstan\": \"^2.1\",\n \"phpunit/phpunit\": \"^11.5.50\",\n \"symfony/phpunit-bridge\": \"^7.0\",\n \"vimeo/psalm\": \"^6.5.0\"\n },\n \"autoload\": {\n \"classmap\": [\n \"database\"\n ],\n \"psr-4\": {\n \"Jiminny\\\\\": \"app/\",\n \"Tests\\\\\": \"tests/\",\n \"Database\\\\Factories\\\\\": \"database/factories/\",\n \"Database\\\\Seeders\\\\\": \"database/seeders/\",\n \"Microsoft\\\\Graph\\\\Generated\\\\Models\\\\\": \"app/Services/MeetingGenerator/Overrides/Microsoft/Graph/Generated/Models/\"\n },\n \"files\": [\n \"app/helpers.php\"\n ]\n },\n \"autoload-dev\": {\n \"classmap\": [\n \"tests/TestCase.php\"\n ],\n \"psr-4\": {\n \"Jiminny\\\\\": \"app/\",\n \"Tests\\\\\": \"tests/\"\n }\n },\n \"scripts\": {\n \"post-root-package-install\": [\n \"php -r \\\"file_exists('.env') || copy('.env.example', '.env');\\\"\"\n ],\n \"post-create-project-cmd\": [\n \"php artisan key:generate --ansi\"\n ],\n \"post-install-cmd\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postInstall\"\n ],\n \"post-update-cmd\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postUpdate\",\n \"php artisan ide-helper:generate\",\n \"php artisan ide-helper:meta\",\n \"@php artisan vendor:publish --tag=laravel-assets --ansi --force\"\n ],\n \"post-autoload-dump\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postAutoloadDump\",\n \"@php artisan package:discover --ansi\"\n ]\n },\n \"config\": {\n \"preferred-install\": \"dist\",\n \"sort-packages\": true,\n \"optimize-autoloader\": true,\n \"allow-plugins\": {\n \"infection/extension-installer\": true,\n \"php-http/discovery\": true,\n \"tbachert/spi\": true\n }\n },\n \"extra\": {\n \"laravel\": {\n \"dont-discover\": [\n \"laravel/dusk\"\n ]\n },\n \"metasyntactical/composer-plugin-license-check\": {\n \"whitelist\": [],\n \"blacklist\": [\n \"AGPL\"\n ]\n }\n },\n \"repositories\": [\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/PHP-FFMpeg/BinaryDriver.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-salesloft.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-aircall.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-pipedrive.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-ringcentral\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-dialpad.git\"\n }\n ],\n \"prefer-stable\": true\n}","depth":4,"value":"{\n \"name\": \"jiminny/app\",\n \"description\": \"The Jiminny Platform.\",\n \"keywords\": [\n \"training\",\n \"salesforce\",\n \"conference\"\n ],\n \"license\": \"MIT\",\n \"type\": \"project\",\n \"require\": {\n \"php\": \"^8.3\",\n \"ext-ctype\": \"*\",\n \"ext-curl\": \"*\",\n \"ext-date\": \"*\",\n \"ext-dom\": \"*\",\n \"ext-fileinfo\": \"*\",\n \"ext-filter\": \"*\",\n \"ext-gd\": \"*\",\n \"ext-gmp\": \"*\",\n \"ext-hash\": \"*\",\n \"ext-iconv\": \"*\",\n \"ext-igbinary\": \"*\",\n \"ext-imagick\": \"*\",\n \"ext-intl\": \"*\",\n \"ext-json\": \"*\",\n \"ext-libxml\": \"*\",\n \"ext-mailparse\": \"*\",\n \"ext-mbstring\": \"*\",\n \"ext-mysqlnd\": \"*\",\n \"ext-openssl\": \"*\",\n \"ext-pcntl\": \"*\",\n \"ext-pcre\": \"*\",\n \"ext-pdo\": \"*\",\n \"ext-pdo_mysql\": \"*\",\n \"ext-phar\": \"*\",\n \"ext-phpiredis\": \"*\",\n \"ext-posix\": \"*\",\n \"ext-readline\": \"*\",\n \"ext-redis\": \"*\",\n \"ext-reflection\": \"*\",\n \"ext-session\": \"*\",\n \"ext-simplexml\": \"*\",\n \"ext-sockets\": \"*\",\n \"ext-spl\": \"*\",\n \"ext-tokenizer\": \"*\",\n \"ext-xml\": \"*\",\n \"ext-xmlreader\": \"*\",\n \"ext-xmlwriter\": \"*\",\n \"ext-zend-opcache\": \"*\",\n \"ext-zip\": \"*\",\n \"ext-zlib\": \"*\",\n \"lib-curl\": \"*\",\n \"lib-curl-openssl\": \"*\",\n \"lib-curl-zlib\": \"*\",\n \"lib-date-timelib\": \"*\",\n \"lib-date-zoneinfo\": \"*\",\n \"lib-fileinfo-libmagic\": \"*\",\n \"lib-gd\": \"*\",\n \"lib-gd-freetype\": \"*\",\n \"lib-gd-libjpeg\": \"*\",\n \"lib-gd-libpng\": \"*\",\n \"lib-gmp\": \"*\",\n \"lib-icu\": \"*\",\n \"lib-icu-cldr\": \"*\",\n \"lib-icu-unicode\": \"*\",\n \"lib-imagick-imagemagick\": \"*\",\n \"lib-libxml\": \"*\",\n \"lib-mbstring-libmbfl\": \"*\",\n \"lib-mbstring-oniguruma\": \"*\",\n \"lib-openssl\": \"*\",\n \"lib-pcre\": \"*\",\n \"lib-pcre-unicode\": \"*\",\n \"lib-zip-libzip\": \"*\",\n \"lib-zlib\": \"*\",\n \"24slides/laravel-saml2\": \"^2.4\",\n \"adam-paterson/oauth2-slack\": \"^1.1\",\n \"asimlqt/php-google-spreadsheet-client\": \"^3.0\",\n \"aws/aws-sdk-php\": \"^3.368\",\n \"aws/aws-sdk-php-laravel\": \"^3.10\",\n \"bepsvpt/secure-headers\": \"^9.0\",\n \"chadhutchins/oauth2-slack\": \"^1.2\",\n \"chaseconey/laravel-datadog-helper\": \"^1.2\",\n \"chrisyue/php-m3u8\": \"4.0.3\",\n \"daniti/oauth2-pipedrive\": \"dev-master\",\n \"devio/pipedrive\": \"^2.6\",\n \"doctrine/dbal\": \"^4.0\",\n \"elasticsearch/elasticsearch\": \"^7.11\",\n \"erusev/parsedown\": \"^1.7\",\n \"fakerphp/faker\": \"^1.23\",\n \"firebase/php-jwt\": \"^7.0\",\n \"flipboxdigital/oauth2-hubspot\": \"1.0.1\",\n \"giggsey/libphonenumber-for-php\": \"^8.12\",\n \"google/apiclient\": \"^2.19\",\n \"google/apiclient-services\": \"~0.360\",\n \"google/apps-meet\": \"^0.5.1\",\n \"guzzlehttp/guzzle\": \"^7.8\",\n \"guzzlehttp/psr7\": \"^2.6\",\n \"halaxa/json-machine\": \"^1.2\",\n \"html2text/html2text\": \"^4.3\",\n \"hubspot/api-client\": \"~5.0.0\",\n \"hubspot/hubspot-php\": \"^5.2.0\",\n \"intercom/intercom-php\": \"^4.5\",\n \"intervention/image\": \"^3.4\",\n \"jakeasmith/http_build_url\": \"^1.0\",\n \"jdavidbakr/cloudfront-proxies\": \"^1.7\",\n \"jeremykendall/php-domain-parser\": \"^6.3\",\n \"jiminny/oauth2-aircall\": \"dev-master\",\n \"jiminny/oauth2-bullhorn\": \"^0.2.0\",\n \"jiminny/oauth2-dialpad\": \"dev-master\",\n \"jiminny/oauth2-salesloft\": \"dev-master\",\n \"jolicode/slack-php-api\": \"^4.5.0\",\n \"kalnoy/nestedset\": \"*\",\n \"laravel/framework\": \"^12.28\",\n \"laravel/helpers\": \"^1.7\",\n \"laravel/passport\": \"^13.0\",\n \"laravel/slack-notification-channel\": \"^3.4\",\n \"laravel/tinker\": \"^2.10.1\",\n \"laravel/ui\": \"^4.6\",\n \"laravolt/avatar\": \"^6.1\",\n \"league/flysystem\": \"^3.0\",\n \"league/flysystem-aws-s3-v3\": \"^3.0\",\n \"league/fractal\": \"*\",\n \"league/oauth2-client\": \"^2.7\",\n \"league/oauth2-google\": \"^4.0\",\n \"league/oauth2-linkedin\": \"^5.1\",\n \"league/oauth2-server\": \"^9.2\",\n \"league/statsd\": \"^2.0\",\n \"markrogoyski/math-php\": \"^2.7.0\",\n \"microsoft/microsoft-graph\": \"^2.51\",\n \"monolog/monolog\": \"^3.0\",\n \"nesbot/carbon\": \"^3.8\",\n \"nette/caching\": \"*\",\n \"phlib/sms-length\": \"^2.0\",\n \"php-ffmpeg/php-ffmpeg\": \"^1.2\",\n \"php-http/client-common\": \"^2.7\",\n \"php-http/curl-client\": \"^2.3\",\n \"php-http/httplug\": \"^2.2\",\n \"php-http/message\": \"^1.16\",\n \"phpseclib/phpseclib\": \"^3.0.36\",\n \"propaganistas/laravel-phone\": \"^5.3\",\n \"psr/cache\": \"^3.0\",\n \"psr/http-message\": \"^2.0\",\n \"psr/log\": \"^3.0\",\n \"psr/simple-cache\": \"^3.0\",\n \"pusher/pusher-php-server\": \"7.2.3\",\n \"ramsey/uuid\": \"^4.2\",\n \"ringcentral/ringcentral-php\": \"3.0.0\",\n \"rmccue/requests\": \"^2.0\",\n \"ruflin/elastica\": \"^7.1.1\",\n \"santigarcor/laratrust\": \"^8.4\",\n \"sentry/sentry\": \"4.13.0\",\n \"sentry/sentry-laravel\": \"~4.13.0\",\n \"shiftonelabs/laravel-sqs-fifo-queue\": \"^3.0\",\n \"spatie/fractalistic\": \"^2.9\",\n \"spatie/laravel-fractal\": \"^6.3\",\n \"spatie/laravel-ignition\": \"^2.9\",\n \"spatie/laravel-webhook-server\": \"^3.8\",\n \"staudenmeir/belongs-to-through\": \"^2.17\",\n \"stevenmaguire/oauth2-salesforce\": \"^2.0\",\n \"symfony/cache\": \"^7.2\",\n \"symfony/console\": \"^7.2\",\n \"symfony/css-selector\": \"^7.2\",\n \"symfony/debug\": \"^4.4\",\n \"symfony/dom-crawler\": \"^7.2\",\n \"symfony/expression-language\": \"^7.2\",\n \"symfony/finder\": \"^7.2\",\n \"symfony/http-client\": \"^7.3\",\n \"symfony/http-foundation\": \"^7.2\",\n \"symfony/http-kernel\": \"^7.2\",\n \"symfony/postmark-mailer\": \"^7.3\",\n \"symfony/process\": \"^7.3\",\n \"symfony/property-access\": \"^7.2\",\n \"symfony/psr-http-message-bridge\": \"^7.0\",\n \"symfony/var-dumper\": \"^7.2\",\n \"symfony/workflow\": \"^7.2\",\n \"tecnickcom/tcpdf\": \"^6.11\",\n \"thenetworg/oauth2-azure\": \"dev-master\",\n \"tmannherz/oauth2-ringcentral\": \"dev-master\",\n \"twilio/sdk\": \"^8.3\",\n \"vanderlee/php-sentence\": \"^1.0\",\n \"vinkla/hashids\": \"^13.0\",\n \"vlucas/phpdotenv\": \"^5.4\",\n \"wildbit/postmark-php\": \"^6.0\",\n \"willdurand/email-reply-parser\": \"^2.8\",\n \"zbateson/mail-mime-parser\": \"^3.0.4\"\n },\n \"require-dev\": {\n \"barryvdh/laravel-debugbar\": \"^3.15\",\n \"barryvdh/laravel-ide-helper\": \"^3.5\",\n \"brianium/paratest\": \"^7.5\",\n \"browserstack/browserstack-local\": \"^1.1.0\",\n \"filp/whoops\": \"^2.9\",\n \"friendsofphp/php-cs-fixer\": \"^3.66\",\n \"infection/infection\": \"^0.29.14\",\n \"jasonmccreary/laravel-test-assertions\": \"^2.5\",\n \"larastan/larastan\": \"^3.1\",\n \"maglnet/composer-require-checker\": \"^4.8\",\n \"mockery/mockery\": \"^1.6\",\n \"nunomaduro/collision\": \"^8.6\",\n \"phpstan/phpstan\": \"^2.1\",\n \"phpunit/phpunit\": \"^11.5.50\",\n \"symfony/phpunit-bridge\": \"^7.0\",\n \"vimeo/psalm\": \"^6.5.0\"\n },\n \"autoload\": {\n \"classmap\": [\n \"database\"\n ],\n \"psr-4\": {\n \"Jiminny\\\\\": \"app/\",\n \"Tests\\\\\": \"tests/\",\n \"Database\\\\Factories\\\\\": \"database/factories/\",\n \"Database\\\\Seeders\\\\\": \"database/seeders/\",\n \"Microsoft\\\\Graph\\\\Generated\\\\Models\\\\\": \"app/Services/MeetingGenerator/Overrides/Microsoft/Graph/Generated/Models/\"\n },\n \"files\": [\n \"app/helpers.php\"\n ]\n },\n \"autoload-dev\": {\n \"classmap\": [\n \"tests/TestCase.php\"\n ],\n \"psr-4\": {\n \"Jiminny\\\\\": \"app/\",\n \"Tests\\\\\": \"tests/\"\n }\n },\n \"scripts\": {\n \"post-root-package-install\": [\n \"php -r \\\"file_exists('.env') || copy('.env.example', '.env');\\\"\"\n ],\n \"post-create-project-cmd\": [\n \"php artisan key:generate --ansi\"\n ],\n \"post-install-cmd\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postInstall\"\n ],\n \"post-update-cmd\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postUpdate\",\n \"php artisan ide-helper:generate\",\n \"php artisan ide-helper:meta\",\n \"@php artisan vendor:publish --tag=laravel-assets --ansi --force\"\n ],\n \"post-autoload-dump\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postAutoloadDump\",\n \"@php artisan package:discover --ansi\"\n ]\n },\n \"config\": {\n \"preferred-install\": \"dist\",\n \"sort-packages\": true,\n \"optimize-autoloader\": true,\n \"allow-plugins\": {\n \"infection/extension-installer\": true,\n \"php-http/discovery\": true,\n \"tbachert/spi\": true\n }\n },\n \"extra\": {\n \"laravel\": {\n \"dont-discover\": [\n \"laravel/dusk\"\n ]\n },\n \"metasyntactical/composer-plugin-license-check\": {\n \"whitelist\": [],\n \"blacklist\": [\n \"AGPL\"\n ]\n }\n },\n \"repositories\": [\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/PHP-FFMpeg/BinaryDriver.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-salesloft.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-aircall.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-pipedrive.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-ringcentral\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-dialpad.git\"\n }\n ],\n \"prefer-stable\": true\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Install","depth":3,"help_text":"Installs packages from composer.json, taking account of composer.lock","role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Update","depth":3,"help_text":"Installs latest appropriate versions of packages from composer.json","role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Show log","depth":3,"help_text":"Show log of Composer-related actions","role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8208934907902466404
|
8387909973189745310
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
18
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Providers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
use Jiminny\Component\DealRisks\DealRisk;
use Jiminny\Exceptions\LogicException;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Controllers\CustomerApi\CustomerApiController;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Ai\AiScorecard;
use Jiminny\Models\Ai\AiScorecardRule;
use Jiminny\Models\Ai\CrmTemplate;
use Jiminny\Models\Ai\CrmTemplateField;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\Group;
use Jiminny\Models\Invitation;
use Jiminny\Models\JobTitle;
use Jiminny\Models\Nudge;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Models\Session;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
class RouteServiceProvider extends ServiceProvider
{
/**
* Define your route model bindings, pattern filters, etc.
*/
public function boot(): void
{
$this->configureRateLimiting();
$this->routes(function (Router $router) {
$this
->mapHealthRoutes($router)
->mapWebhookRoutes($router)
->mapApiRoutes($router)
->mapApiV2Routes($router)
->mapWebRoutes($router)
->mapScimRoutes($router)
->mapCustomerApiRoutes($router)
->mapEmbeddedRoutes($router)
;
});
$this->defineRouteBindings();
}
private function mapHealthRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
], static function (Router $router) {
require base_path('routes/health.php');
});
return $this;
}
/**
* Define the route model bindings.
*/
protected function defineRouteBindings()
{
Route::pattern('teamSlug', '^[.\-_a-z0-9]*$');
Route::pattern('userSlug', '^[.\-_a-z0-9]*$');
Route::bind('participant', function ($value) {
return Participant::uuid($value);
});
Route::bind('participantA', static fn (string $value): Participant => Participant::uuid($value));
Route::bind('participantB', static fn (string $value): Participant => Participant::uuid($value));
Route::bind('connection', function ($value) {
return Connection::uuid($value);
});
Route::bind('activity', function ($value) {
return Activity::uuid($value);
});
Route::bind('session', function (string $value) {
return Session::uuid($value);
});
Route::bind('transcription', function ($value) {
return Activity\Transcription::uuid($value);
});
Route::bind('transcriptionProvider', static function (string $scorecardUuid): Models\TranscriptionProvider {
return Models\TranscriptionProvider::where(
'uuid',
Models\TranscriptionProvider::toOptimized($scorecardUuid)
)->firstOrFail();
});
Route::bind('coachingFeedback', function ($value) {
return CoachingFeedback::uuid($value);
});
Route::bind('team', function ($value) {
return Team::uuid($value);
});
Route::bind('crmTemplate', fn ($value) => CrmTemplate::uuid($value));
Route::bind('crmTemplateField', fn ($value) => CrmTemplateField::uuid($value));
Route::bind('aiScorecard', fn ($value) => AiScorecard::uuid($value));
Route::bind('aiScorecardRule', fn ($value) => AiScorecardRule::uuid($value));
Route::bind('group', function ($value) {
return Group::uuid($value);
});
Route::bind('user', function ($value) {
return User::uuid($value);
});
Route::bind('comment', function (string $value, \Illuminate\Routing\Route $route) {
$targetController = $route->getController();
if (! $targetController instanceof CommentContextInterface) {
throw new LogicException(
'Type hinting comment will require additional effort due to polymorphism.'
. ' Either implement CommentContextInterface or inject $commentId and process manually',
);
}
$commentClass = $targetController::getCommentImplementation();
return $commentClass::uuid($value);
});
Route::bind('track', function ($value) {
return Track::uuid($value);
});
Route::bind('invitation', function ($value) {
return Invitation::uuid($value);
});
Route::bind('playbook', function ($value) {
return Playbook::uuid($value);
});
Route::bind('category', function ($value) {
return PlaybookCategory::uuid($value);
});
Route::bind('coachingSection', function ($value) {
return CoachingSection::uuid($value);
});
Route::bind('coachingSectionCriterion', function ($value) {
return Models\CoachingSectionCriterion::uuid($value);
});
Route::bind('playlist', function ($value) {
return Playlist::uuid($value);
});
Route::bind('playlistActivity', static fn (string $value) => PlaylistActivity::uuid($value));
Route::bind('playlistTrack', static fn (string $value) => PlaylistActivity::uuid($value));
Route::bind('playlistShare', static fn (string $uuid) => Playlist\Share::uuid($uuid));
Route::bind('job', function ($value) {
return JobTitle::uuid($value);
});
Route::bind('notification', function ($value) {
return Activity\AvailabilityNotification::uuid($value);
});
Route::bind('activityMoment', function ($value) {
return Activity\Moment::uuid($value);
});
Route::bind('search', function ($value) {
return Activity\Search::uuid($value);
});
Route::bind('nudge', static function ($value): Nudge {
return Nudge::uuid($value);
});
Route::bind('theme', static function ($value): Models\PlaybackTheme {
return Models\PlaybackTheme::uuid($value);
});
Route::bind('topic', static function ($value): Models\PlaybackTheme\Topic {
return Models\PlaybackTheme\Topic::uuid($value);
});
Route::bind('topicTrigger', static function ($value): Models\PlaybackTheme\TopicTrigger {
return Models\PlaybackTheme\TopicTrigger::uuid($value);
});
Route::bind('vocabulary', static function (string $id): Models\Vocabulary {
return Models\Vocabulary::uuid($id);
});
Route::bind('teamDomain', function ($value) {
return Models\TeamDomain::uuid($value);
});
Route::bind('opportunity', function ($value) {
return Opportunity::uuid($value);
});
Route::bind('dealRisk', function ($value) {
return DealRisk::uuid($value);
});
Route::bind('layout', static fn (string $uuid) => Models\Crm\Layout::uuid($uuid));
Route::bind('scorecard', static function (string $scorecardUuid): Models\Scorecard\Scorecard {
return Models\Scorecard\Scorecard::where(
'uuid',
Models\Scorecard\Scorecard::toOptimized($scorecardUuid)
)->firstOrFail();
});
Route::bind('scorecardRule', static function (string $scorecardRuleUuid): Models\Scorecard\ScorecardRule {
return Models\Scorecard\ScorecardRule::where(
'uuid',
Models\Scorecard\ScorecardRule::toOptimized($scorecardRuleUuid)
)->firstOrFail();
});
Route::bind('askAnythingPrompt', function ($value) {
return Models\AskAnything\AskAnythingPrompt::uuid($value);
});
}
/**
* Define the routes for the application.
*
* @param \Illuminate\Routing\Router $router
*/
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*/
private function mapWebRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['web'],
], static function (Router $router) {
require base_path('routes/web.php');
});
return $this;
}
/**
* Define the "Embedded" routes for the application.
*
* These routes depend on a Partitioned cookie,
* and are accessible only after login.
*/
private function mapEmbeddedRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['embedded'],
'prefix' => 'embedded',
], static function (Router $router) {
require base_path('routes/embedded.php');
});
return $this;
}
private function mapWebhookRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['webhook'],
'prefix' => 'webhook',
], function (Router $router) {
require base_path('routes/webhook.php');
});
return $this;
}
/**
* Define the "api" routes for the application.
*/
private function mapApiRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['api'],
'prefix' => 'api/v1',
], function (Router $router) {
require base_path('routes/api.php');
});
return $this;
}
private function mapApiV2Routes(Router $router): self
{
$router->group([
'middleware' => ['api', 'auth:api'],
'prefix' => 'api/v2',
], static function (Router $router) {
require base_path('routes/api_v2.php');
});
return $this;
}
private function mapScimRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['scim'],
'prefix' => 'scim/v2',
], function (Router $router) {
require base_path('routes/scim.php');
});
return $this;
}
private function mapCustomerApiRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['customer-api'],
'prefix' => 'customer/api/v1',
], function (Router $router) {
require base_path('routes/customer_api.php');
});
return $this;
}
/** Configure the rate limiters for the application. */
protected function configureRateLimiting(): void
{
RateLimiter::for('api', static function (Request $request) {
$user = $request->user();
if ($user instanceof User || $user instanceof Models\Partner) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
if (! is_string($subject)) {
throw new LogicException('Unable to identify subject to rate limit');
}
return Limit::perMinute(120)->by($subject);
});
RateLimiter::for('customer-api', static function (Request $request) {
// Get the token using the same method as the middleware
$token = null;
if ($request->bearerToken() !== null) {
$token = $request->bearerToken();
} elseif ($request->get('api_token') !== null) {
$token = $request->get('api_token');
} elseif ($request->post('api_token') !== null) {
$token = $request->post('api_token');
}
// If we have a valid token, get the team ID for rate limiting
if ($token !== null && strlen($token) === Team::CUSTOMER_API_TOKEN_LENGTH) {
$tokens = Cache::remember(Team::CUSTOMER_API_TOKENS_CACHE, 60 * 60, static function () {
return Team::query()->whereNotNull('api_token')->pluck('id', 'api_token');
});
$hashedToken = hash('sha256', $token);
if (is_countable($tokens) && ! empty($tokens) && isset($tokens[$hashedToken])) {
$teamId = $tokens[$hashedToken];
$team = Team::find($teamId);
if ($team) {
$subject = $team->getUuid();
// Special rate limit for Funding Circle and Cision.
if (in_array($subject, [
'9762611a-fc86-4234-baba-a436de26e774', '1151b5b0-0680-4190-b30c-72649280837d',
'16b50903-a115-448f-877c-6c01e8b45f5d', 'bfc1c304-83b2-47a3-a7ce-1d28b0e1e90e',
'447ee473-7dd8-41bc-a702-9322e5b0ec5b'])) {
// Route-specific rate limiting
if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {
return Limit::perMinute(40)->by($subject);
}
return Limit::perMinute(1000)->by($subject);
}
// Route-specific rate limiting
if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {
return Limit::perMinute(30)->by($subject);
}
return Limit::perMinute(120)->by($subject);
}
}
}
// Default fallback to IP-based limiting
return Limit::perMinute(120)->by($request->ip());
});
RateLimiter::for('conference-consent', static function (Request $request) {
// Rate limit by IP address for public consent endpoint
// Allow 10 requests per minute per IP to prevent abuse while allowing legitimate use
return Limit::perMinute(10)->by($request->ip());
});
RateLimiter::for('activity-export-shareable-link', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(30)->by($subject);
});
RateLimiter::for('activity-export', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(10)->by($subject);
});
RateLimiter::for('transcription-download', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(3)->by($subject);
});
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
14
Previous Highlighted Error
Next Highlighted Error
{
"name": "jiminny/app",
"description": "The Jiminny Platform.",
"keywords": [
"training",
"salesforce",
"conference"
],
"license": "MIT",
"type": "project",
"require": {
"php": "^8.3",
"ext-ctype": "*",
"ext-curl": "*",
"ext-date": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-filter": "*",
"ext-gd": "*",
"ext-gmp": "*",
"ext-hash": "*",
"ext-iconv": "*",
"ext-igbinary": "*",
"ext-imagick": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mailparse": "*",
"ext-mbstring": "*",
"ext-mysqlnd": "*",
"ext-openssl": "*",
"ext-pcntl": "*",
"ext-pcre": "*",
"ext-pdo": "*",
"ext-pdo_mysql": "*",
"ext-phar": "*",
"ext-phpiredis": "*",
"ext-posix": "*",
"ext-readline": "*",
"ext-redis": "*",
"ext-reflection": "*",
"ext-session": "*",
"ext-simplexml": "*",
"ext-sockets": "*",
"ext-spl": "*",
"ext-tokenizer": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zend-opcache": "*",
"ext-zip": "*",
"ext-zlib": "*",
"lib-curl": "*",
"lib-curl-openssl": "*",
"lib-curl-zlib": "*",
"lib-date-timelib": "*",
"lib-date-zoneinfo": "*",
"lib-fileinfo-libmagic": "*",
"lib-gd": "*",
"lib-gd-freetype": "*",
"lib-gd-libjpeg": "*",
"lib-gd-libpng": "*",
"lib-gmp": "*",
"lib-icu": "*",
"lib-icu-cldr": "*",
"lib-icu-unicode": "*",
"lib-imagick-imagemagick": "*",
"lib-libxml": "*",
"lib-mbstring-libmbfl": "*",
"lib-mbstring-oniguruma": "*",
"lib-openssl": "*",
"lib-pcre": "*",
"lib-pcre-unicode": "*",
"lib-zip-libzip": "*",
"lib-zlib": "*",
"24slides/laravel-saml2": "^2.4",
"adam-paterson/oauth2-slack": "^1.1",
"asimlqt/php-google-spreadsheet-client": "^3.0",
"aws/aws-sdk-php": "^3.368",
"aws/aws-sdk-php-laravel": "^3.10",
"bepsvpt/secure-headers": "^9.0",
"chadhutchins/oauth2-slack": "^1.2",
"chaseconey/laravel-datadog-helper": "^1.2",
"chrisyue/php-m3u8": "4.0.3",
"daniti/oauth2-pipedrive": "dev-master",
"devio/pipedrive": "^2.6",
"doctrine/dbal": "^4.0",
"elasticsearch/elasticsearch": "^7.11",
"erusev/parsedown": "^1.7",
"fakerphp/faker": "^1.23",
"firebase/php-jwt": "^7.0",
"flipboxdigital/oauth2-hubspot": "1.0.1",
"giggsey/libphonenumber-for-php": "^8.12",
"google/apiclient": "^2.19",
"google/apiclient-services": "~0.360",
"google/apps-meet": "^0.5.1",
"guzzlehttp/guzzle": "^7.8",
"guzzlehttp/psr7": "^2.6",
"halaxa/json-machine": "^1.2",
"html2text/html2text": "^4.3",
"hubspot/api-client": "~5.0.0",
"hubspot/hubspot-php": "^5.2.0",
"intercom/intercom-php": "^4.5",
"intervention/image": "^3.4",
"jakeasmith/http_build_url": "^1.0",
"jdavidbakr/cloudfront-proxies": "^1.7",
"jeremykendall/php-domain-parser": "^6.3",
"jiminny/oauth2-aircall": "dev-master",
"jiminny/oauth2-bullhorn": "^0.2.0",
"jiminny/oauth2-dialpad": "dev-master",
"jiminny/oauth2-salesloft": "dev-master",
"jolicode/slack-php-api": "^4.5.0",
"kalnoy/nestedset": "*",
"laravel/framework": "^12.28",
"laravel/helpers": "^1.7",
"laravel/passport": "^13.0",
"laravel/slack-notification-channel": "^3.4",
"laravel/tinker": "^2.10.1",
"laravel/ui": "^4.6",
"laravolt/avatar": "^6.1",
"league/flysystem": "^3.0",
"league/flysystem-aws-s3-v3": "^3.0",
"league/fractal": "*",
"league/oauth2-client": "^2.7",
"league/oauth2-google": "^4.0",
"league/oauth2-linkedin": "^5.1",
"league/oauth2-server": "^9.2",
"league/statsd": "^2.0",
"markrogoyski/math-php": "^2.7.0",
"microsoft/microsoft-graph": "^2.51",
"monolog/monolog": "^3.0",
"nesbot/carbon": "^3.8",
"nette/caching": "*",
"phlib/sms-length": "^2.0",
"php-ffmpeg/php-ffmpeg": "^1.2",
"php-http/client-common": "^2.7",
"php-http/curl-client": "^2.3",
"php-http/httplug": "^2.2",
"php-http/message": "^1.16",
"phpseclib/phpseclib": "^3.0.36",
"propaganistas/laravel-phone": "^5.3",
"psr/cache": "^3.0",
"psr/http-message": "^2.0",
"psr/log": "^3.0",
"psr/simple-cache": "^3.0",
"pusher/pusher-php-server": "7.2.3",
"ramsey/uuid": "^4.2",
"ringcentral/ringcentral-php": "3.0.0",
"rmccue/requests": "^2.0",
"ruflin/elastica": "^7.1.1",
"santigarcor/laratrust": "^8.4",
"sentry/sentry": "4.13.0",
"sentry/sentry-laravel": "~4.13.0",
"shiftonelabs/laravel-sqs-fifo-queue": "^3.0",
"spatie/fractalistic": "^2.9",
"spatie/laravel-fractal": "^6.3",
"spatie/laravel-ignition": "^2.9",
"spatie/laravel-webhook-server": "^3.8",
"staudenmeir/belongs-to-through": "^2.17",
"stevenmaguire/oauth2-salesforce": "^2.0",
"symfony/cache": "^7.2",
"symfony/console": "^7.2",
"symfony/css-selector": "^7.2",
"symfony/debug": "^4.4",
"symfony/dom-crawler": "^7.2",
"symfony/expression-language": "^7.2",
"symfony/finder": "^7.2",
"symfony/http-client": "^7.3",
"symfony/http-foundation": "^7.2",
"symfony/http-kernel": "^7.2",
"symfony/postmark-mailer": "^7.3",
"symfony/process": "^7.3",
"symfony/property-access": "^7.2",
"symfony/psr-http-message-bridge": "^7.0",
"symfony/var-dumper": "^7.2",
"symfony/workflow": "^7.2",
"tecnickcom/tcpdf": "^6.11",
"thenetworg/oauth2-azure": "dev-master",
"tmannherz/oauth2-ringcentral": "dev-master",
"twilio/sdk": "^8.3",
"vanderlee/php-sentence": "^1.0",
"vinkla/hashids": "^13.0",
"vlucas/phpdotenv": "^5.4",
"wildbit/postmark-php": "^6.0",
"willdurand/email-reply-parser": "^2.8",
"zbateson/mail-mime-parser": "^3.0.4"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.15",
"barryvdh/laravel-ide-helper": "^3.5",
"brianium/paratest": "^7.5",
"browserstack/browserstack-local": "^1.1.0",
"filp/whoops": "^2.9",
"friendsofphp/php-cs-fixer": "^3.66",
"infection/infection": "^0.29.14",
"jasonmccreary/laravel-test-assertions": "^2.5",
"larastan/larastan": "^3.1",
"maglnet/composer-require-checker": "^4.8",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^11.5.50",
"symfony/phpunit-bridge": "^7.0",
"vimeo/psalm": "^6.5.0"
},
"autoload": {
"classmap": [
"database"
],
"psr-4": {
"Jiminny\\": "app/",
"Tests\\": "tests/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/",
"Microsoft\\Graph\\Generated\\Models\\": "app/Services/MeetingGenerator/Overrides/Microsoft/Graph/Generated/Models/"
},
"files": [
"app/helpers.php"
]
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
],
"psr-4": {
"Jiminny\\": "app/",
"Tests\\": "tests/"
}
},
"scripts": {
"post-root-package-install": [
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"php artisan key:generate --ansi"
],
"post-install-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postInstall"
],
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan ide-helper:generate",
"php artisan ide-helper:meta",
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
]
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true,
"allow-plugins": {
"infection/extension-installer": true,
"php-http/discovery": true,
"tbachert/spi": true
}
},
"extra": {
"laravel": {
"dont-discover": [
"laravel/dusk"
]
},
"metasyntactical/composer-plugin-license-check": {
"whitelist": [],
"blacklist": [
"AGPL"
]
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/PHP-FFMpeg/BinaryDriver.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-salesloft.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-aircall.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-pipedrive.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-ringcentral"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-dialpad.git"
}
],
"prefer-stable": true
}
Install
Update
Show log
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
53782
|
1163
|
35
|
2026-04-20T08:28:04.016951+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-20/1776 /Users/lukas/.screenpipe/data/data/2026-04-20/1776673684016_m2.jpg...
|
PhpStorm
|
faVsco.js – RouteServiceProvider.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
18
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Providers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
use Jiminny\Component\DealRisks\DealRisk;
use Jiminny\Exceptions\LogicException;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Controllers\CustomerApi\CustomerApiController;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Ai\AiScorecard;
use Jiminny\Models\Ai\AiScorecardRule;
use Jiminny\Models\Ai\CrmTemplate;
use Jiminny\Models\Ai\CrmTemplateField;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\Group;
use Jiminny\Models\Invitation;
use Jiminny\Models\JobTitle;
use Jiminny\Models\Nudge;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Models\Session;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
class RouteServiceProvider extends ServiceProvider
{
/**
* Define your route model bindings, pattern filters, etc.
*/
public function boot(): void
{
$this->configureRateLimiting();
$this->routes(function (Router $router) {
$this
->mapHealthRoutes($router)
->mapWebhookRoutes($router)
->mapApiRoutes($router)
->mapApiV2Routes($router)
->mapWebRoutes($router)
->mapScimRoutes($router)
->mapCustomerApiRoutes($router)
->mapEmbeddedRoutes($router)
;
});
$this->defineRouteBindings();
}
private function mapHealthRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
], static function (Router $router) {
require base_path('routes/health.php');
});
return $this;
}
/**
* Define the route model bindings.
*/
protected function defineRouteBindings()
{
Route::pattern('teamSlug', '^[.\-_a-z0-9]*$');
Route::pattern('userSlug', '^[.\-_a-z0-9]*$');
Route::bind('participant', function ($value) {
return Participant::uuid($value);
});
Route::bind('participantA', static fn (string $value): Participant => Participant::uuid($value));
Route::bind('participantB', static fn (string $value): Participant => Participant::uuid($value));
Route::bind('connection', function ($value) {
return Connection::uuid($value);
});
Route::bind('activity', function ($value) {
return Activity::uuid($value);
});
Route::bind('session', function (string $value) {
return Session::uuid($value);
});
Route::bind('transcription', function ($value) {
return Activity\Transcription::uuid($value);
});
Route::bind('transcriptionProvider', static function (string $scorecardUuid): Models\TranscriptionProvider {
return Models\TranscriptionProvider::where(
'uuid',
Models\TranscriptionProvider::toOptimized($scorecardUuid)
)->firstOrFail();
});
Route::bind('coachingFeedback', function ($value) {
return CoachingFeedback::uuid($value);
});
Route::bind('team', function ($value) {
return Team::uuid($value);
});
Route::bind('crmTemplate', fn ($value) => CrmTemplate::uuid($value));
Route::bind('crmTemplateField', fn ($value) => CrmTemplateField::uuid($value));
Route::bind('aiScorecard', fn ($value) => AiScorecard::uuid($value));
Route::bind('aiScorecardRule', fn ($value) => AiScorecardRule::uuid($value));
Route::bind('group', function ($value) {
return Group::uuid($value);
});
Route::bind('user', function ($value) {
return User::uuid($value);
});
Route::bind('comment', function (string $value, \Illuminate\Routing\Route $route) {
$targetController = $route->getController();
if (! $targetController instanceof CommentContextInterface) {
throw new LogicException(
'Type hinting comment will require additional effort due to polymorphism.'
. ' Either implement CommentContextInterface or inject $commentId and process manually',
);
}
$commentClass = $targetController::getCommentImplementation();
return $commentClass::uuid($value);
});
Route::bind('track', function ($value) {
return Track::uuid($value);
});
Route::bind('invitation', function ($value) {
return Invitation::uuid($value);
});
Route::bind('playbook', function ($value) {
return Playbook::uuid($value);
});
Route::bind('category', function ($value) {
return PlaybookCategory::uuid($value);
});
Route::bind('coachingSection', function ($value) {
return CoachingSection::uuid($value);
});
Route::bind('coachingSectionCriterion', function ($value) {
return Models\CoachingSectionCriterion::uuid($value);
});
Route::bind('playlist', function ($value) {
return Playlist::uuid($value);
});
Route::bind('playlistActivity', static fn (string $value) => PlaylistActivity::uuid($value));
Route::bind('playlistTrack', static fn (string $value) => PlaylistActivity::uuid($value));
Route::bind('playlistShare', static fn (string $uuid) => Playlist\Share::uuid($uuid));
Route::bind('job', function ($value) {
return JobTitle::uuid($value);
});
Route::bind('notification', function ($value) {
return Activity\AvailabilityNotification::uuid($value);
});
Route::bind('activityMoment', function ($value) {
return Activity\Moment::uuid($value);
});
Route::bind('search', function ($value) {
return Activity\Search::uuid($value);
});
Route::bind('nudge', static function ($value): Nudge {
return Nudge::uuid($value);
});
Route::bind('theme', static function ($value): Models\PlaybackTheme {
return Models\PlaybackTheme::uuid($value);
});
Route::bind('topic', static function ($value): Models\PlaybackTheme\Topic {
return Models\PlaybackTheme\Topic::uuid($value);
});
Route::bind('topicTrigger', static function ($value): Models\PlaybackTheme\TopicTrigger {
return Models\PlaybackTheme\TopicTrigger::uuid($value);
});
Route::bind('vocabulary', static function (string $id): Models\Vocabulary {
return Models\Vocabulary::uuid($id);
});
Route::bind('teamDomain', function ($value) {
return Models\TeamDomain::uuid($value);
});
Route::bind('opportunity', function ($value) {
return Opportunity::uuid($value);
});
Route::bind('dealRisk', function ($value) {
return DealRisk::uuid($value);
});
Route::bind('layout', static fn (string $uuid) => Models\Crm\Layout::uuid($uuid));
Route::bind('scorecard', static function (string $scorecardUuid): Models\Scorecard\Scorecard {
return Models\Scorecard\Scorecard::where(
'uuid',
Models\Scorecard\Scorecard::toOptimized($scorecardUuid)
)->firstOrFail();
});
Route::bind('scorecardRule', static function (string $scorecardRuleUuid): Models\Scorecard\ScorecardRule {
return Models\Scorecard\ScorecardRule::where(
'uuid',
Models\Scorecard\ScorecardRule::toOptimized($scorecardRuleUuid)
)->firstOrFail();
});
Route::bind('askAnythingPrompt', function ($value) {
return Models\AskAnything\AskAnythingPrompt::uuid($value);
});
}
/**
* Define the routes for the application.
*
* @param \Illuminate\Routing\Router $router
*/
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*/
private function mapWebRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['web'],
], static function (Router $router) {
require base_path('routes/web.php');
});
return $this;
}
/**
* Define the "Embedded" routes for the application.
*
* These routes depend on a Partitioned cookie,
* and are accessible only after login.
*/
private function mapEmbeddedRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['embedded'],
'prefix' => 'embedded',
], static function (Router $router) {
require base_path('routes/embedded.php');
});
return $this;
}
private function mapWebhookRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['webhook'],
'prefix' => 'webhook',
], function (Router $router) {
require base_path('routes/webhook.php');
});
return $this;
}
/**
* Define the "api" routes for the application.
*/
private function mapApiRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['api'],
'prefix' => 'api/v1',
], function (Router $router) {
require base_path('routes/api.php');
});
return $this;
}
private function mapApiV2Routes(Router $router): self
{
$router->group([
'middleware' => ['api', 'auth:api'],
'prefix' => 'api/v2',
], static function (Router $router) {
require base_path('routes/api_v2.php');
});
return $this;
}
private function mapScimRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['scim'],
'prefix' => 'scim/v2',
], function (Router $router) {
require base_path('routes/scim.php');
});
return $this;
}
private function mapCustomerApiRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['customer-api'],
'prefix' => 'customer/api/v1',
], function (Router $router) {
require base_path('routes/customer_api.php');
});
return $this;
}
/** Configure the rate limiters for the application. */
protected function configureRateLimiting(): void
{
RateLimiter::for('api', static function (Request $request) {
$user = $request->user();
if ($user instanceof User || $user instanceof Models\Partner) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
if (! is_string($subject)) {
throw new LogicException('Unable to identify subject to rate limit');
}
return Limit::perMinute(120)->by($subject);
});
RateLimiter::for('customer-api', static function (Request $request) {
// Get the token using the same method as the middleware
$token = null;
if ($request->bearerToken() !== null) {
$token = $request->bearerToken();
} elseif ($request->get('api_token') !== null) {
$token = $request->get('api_token');
} elseif ($request->post('api_token') !== null) {
$token = $request->post('api_token');
}
// If we have a valid token, get the team ID for rate limiting
if ($token !== null && strlen($token) === Team::CUSTOMER_API_TOKEN_LENGTH) {
$tokens = Cache::remember(Team::CUSTOMER_API_TOKENS_CACHE, 60 * 60, static function () {
return Team::query()->whereNotNull('api_token')->pluck('id', 'api_token');
});
$hashedToken = hash('sha256', $token);
if (is_countable($tokens) && ! empty($tokens) && isset($tokens[$hashedToken])) {
$teamId = $tokens[$hashedToken];
$team = Team::find($teamId);
if ($team) {
$subject = $team->getUuid();
// Special rate limit for Funding Circle and Cision.
if (in_array($subject, [
'9762611a-fc86-4234-baba-a436de26e774', '1151b5b0-0680-4190-b30c-72649280837d',
'16b50903-a115-448f-877c-6c01e8b45f5d', 'bfc1c304-83b2-47a3-a7ce-1d28b0e1e90e',
'447ee473-7dd8-41bc-a702-9322e5b0ec5b'])) {
// Route-specific rate limiting
if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {
return Limit::perMinute(40)->by($subject);
}
return Limit::perMinute(1000)->by($subject);
}
// Route-specific rate limiting
if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {
return Limit::perMinute(30)->by($subject);
}
return Limit::perMinute(120)->by($subject);
}
}
}
// Default fallback to IP-based limiting
return Limit::perMinute(120)->by($request->ip());
});
RateLimiter::for('conference-consent', static function (Request $request) {
// Rate limit by IP address for public consent endpoint
// Allow 10 requests per minute per IP to prevent abuse while allowing legitimate use
return Limit::perMinute(10)->by($request->ip());
});
RateLimiter::for('activity-export-shareable-link', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(30)->by($subject);
});
RateLimiter::for('activity-export', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(10)->by($subject);
});
RateLimiter::for('transcription-download', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(3)->by($subject);
});
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
14
Previous Highlighted Error
Next Highlighted Error
{
"name": "jiminny/app",
"description": "The Jiminny Platform.",
"keywords": [
"training",
"salesforce",
"conference"
],
"license": "MIT",
"type": "project",
"require": {
"php": "^8.3",
"ext-ctype": "*",
"ext-curl": "*",
"ext-date": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-filter": "*",
"ext-gd": "*",
"ext-gmp": "*",
"ext-hash": "*",
"ext-iconv": "*",
"ext-igbinary": "*",
"ext-imagick": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mailparse": "*",
"ext-mbstring": "*",
"ext-mysqlnd": "*",
"ext-openssl": "*",
"ext-pcntl": "*",
"ext-pcre": "*",
"ext-pdo": "*",
"ext-pdo_mysql": "*",
"ext-phar": "*",
"ext-phpiredis": "*",
"ext-posix": "*",
"ext-readline": "*",
"ext-redis": "*",
"ext-reflection": "*",
"ext-session": "*",
"ext-simplexml": "*",
"ext-sockets": "*",
"ext-spl": "*",
"ext-tokenizer": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zend-opcache": "*",
"ext-zip": "*",
"ext-zlib": "*",
"lib-curl": "*",
"lib-curl-openssl": "*",
"lib-curl-zlib": "*",
"lib-date-timelib": "*",
"lib-date-zoneinfo": "*",
"lib-fileinfo-libmagic": "*",
"lib-gd": "*",
"lib-gd-freetype": "*",
"lib-gd-libjpeg": "*",
"lib-gd-libpng": "*",
"lib-gmp": "*",
"lib-icu": "*",
"lib-icu-cldr": "*",
"lib-icu-unicode": "*",
"lib-imagick-imagemagick": "*",
"lib-libxml": "*",
"lib-mbstring-libmbfl": "*",
"lib-mbstring-oniguruma": "*",
"lib-openssl": "*",
"lib-pcre": "*",
"lib-pcre-unicode": "*",
"lib-zip-libzip": "*",
"lib-zlib": "*",
"24slides/laravel-saml2": "^2.4",
"adam-paterson/oauth2-slack": "^1.1",
"asimlqt/php-google-spreadsheet-client": "^3.0",
"aws/aws-sdk-php": "^3.368",
"aws/aws-sdk-php-laravel": "^3.10",
"bepsvpt/secure-headers": "^9.0",
"chadhutchins/oauth2-slack": "^1.2",
"chaseconey/laravel-datadog-helper": "^1.2",
"chrisyue/php-m3u8": "4.0.3",
"daniti/oauth2-pipedrive": "dev-master",
"devio/pipedrive": "^2.6",
"doctrine/dbal": "^4.0",
"elasticsearch/elasticsearch": "^7.11",
"erusev/parsedown": "^1.7",
"fakerphp/faker": "^1.23",
"firebase/php-jwt": "^7.0",
"flipboxdigital/oauth2-hubspot": "1.0.1",
"giggsey/libphonenumber-for-php": "^8.12",
"google/apiclient": "^2.19",
"google/apiclient-services": "~0.360",
"google/apps-meet": "^0.5.1",
"guzzlehttp/guzzle": "^7.8",
"guzzlehttp/psr7": "^2.6",
"halaxa/json-machine": "^1.2",
"html2text/html2text": "^4.3",
"hubspot/api-client": "~5.0.0",
"hubspot/hubspot-php": "^5.2.0",
"intercom/intercom-php": "^4.5",
"intervention/image": "^3.4",
"jakeasmith/http_build_url": "^1.0",
"jdavidbakr/cloudfront-proxies": "^1.7",
"jeremykendall/php-domain-parser": "^6.3",
"jiminny/oauth2-aircall": "dev-master",
"jiminny/oauth2-bullhorn": "^0.2.0",
"jiminny/oauth2-dialpad": "dev-master",
"jiminny/oauth2-salesloft": "dev-master",
"jolicode/slack-php-api": "^4.5.0",
"kalnoy/nestedset": "*",
"laravel/framework": "^12.28",
"laravel/helpers": "^1.7",
"laravel/passport": "^13.0",
"laravel/slack-notification-channel": "^3.4",
"laravel/tinker": "^2.10.1",
"laravel/ui": "^4.6",
"laravolt/avatar": "^6.1",
"league/flysystem": "^3.0",
"league/flysystem-aws-s3-v3": "^3.0",
"league/fractal": "*",
"league/oauth2-client": "^2.7",
"league/oauth2-google": "^4.0",
"league/oauth2-linkedin": "^5.1",
"league/oauth2-server": "^9.2",
"league/statsd": "^2.0",
"markrogoyski/math-php": "^2.7.0",
"microsoft/microsoft-graph": "^2.51",
"monolog/monolog": "^3.0",
"nesbot/carbon": "^3.8",
"nette/caching": "*",
"phlib/sms-length": "^2.0",
"php-ffmpeg/php-ffmpeg": "^1.2",
"php-http/client-common": "^2.7",
"php-http/curl-client": "^2.3",
"php-http/httplug": "^2.2",
"php-http/message": "^1.16",
"phpseclib/phpseclib": "^3.0.36",
"propaganistas/laravel-phone": "^5.3",
"psr/cache": "^3.0",
"psr/http-message": "^2.0",
"psr/log": "^3.0",
"psr/simple-cache": "^3.0",
"pusher/pusher-php-server": "7.2.3",
"ramsey/uuid": "^4.2",
"ringcentral/ringcentral-php": "3.0.0",
"rmccue/requests": "^2.0",
"ruflin/elastica": "^7.1.1",
"santigarcor/laratrust": "^8.4",
"sentry/sentry": "4.13.0",
"sentry/sentry-laravel": "~4.13.0",
"shiftonelabs/laravel-sqs-fifo-queue": "^3.0",
"spatie/fractalistic": "^2.9",
"spatie/laravel-fractal": "^6.3",
"spatie/laravel-ignition": "^2.9",
"spatie/laravel-webhook-server": "^3.8",
"staudenmeir/belongs-to-through": "^2.17",
"stevenmaguire/oauth2-salesforce": "^2.0",
"symfony/cache": "^7.2",
"symfony/console": "^7.2",
"symfony/css-selector": "^7.2",
"symfony/debug": "^4.4",
"symfony/dom-crawler": "^7.2",
"symfony/expression-language": "^7.2",
"symfony/finder": "^7.2",
"symfony/http-client": "^7.3",
"symfony/http-foundation": "^7.2",
"symfony/http-kernel": "^7.2",
"symfony/postmark-mailer": "^7.3",
"symfony/process": "^7.3",
"symfony/property-access": "^7.2",
"symfony/psr-http-message-bridge": "^7.0",
"symfony/var-dumper": "^7.2",
"symfony/workflow": "^7.2",
"tecnickcom/tcpdf": "^6.11",
"thenetworg/oauth2-azure": "dev-master",
"tmannherz/oauth2-ringcentral": "dev-master",
"twilio/sdk": "^8.3",
"vanderlee/php-sentence": "^1.0",
"vinkla/hashids": "^13.0",
"vlucas/phpdotenv": "^5.4",
"wildbit/postmark-php": "^6.0",
"willdurand/email-reply-parser": "^2.8",
"zbateson/mail-mime-parser": "^3.0.4"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.15",
"barryvdh/laravel-ide-helper": "^3.5",
"brianium/paratest": "^7.5",
"browserstack/browserstack-local": "^1.1.0",
"filp/whoops": "^2.9",
"friendsofphp/php-cs-fixer": "^3.66",
"infection/infection": "^0.29.14",
"jasonmccreary/laravel-test-assertions": "^2.5",
"larastan/larastan": "^3.1",
"maglnet/composer-require-checker": "^4.8",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^11.5.50",
"symfony/phpunit-bridge": "^7.0",
"vimeo/psalm": "^6.5.0"
},
"autoload": {
"classmap": [
"database"
],
"psr-4": {
"Jiminny\\": "app/",
"Tests\\": "tests/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/",
"Microsoft\\Graph\\Generated\\Models\\": "app/Services/MeetingGenerator/Overrides/Microsoft/Graph/Generated/Models/"
},
"files": [
"app/helpers.php"
]
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
],
"psr-4": {
"Jiminny\\": "app/",
"Tests\\": "tests/"
}
},
"scripts": {
"post-root-package-install": [
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"php artisan key:generate --ansi"
],
"post-install-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postInstall"
],
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan ide-helper:generate",
"php artisan ide-helper:meta",
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
]
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true,
"allow-plugins": {
"infection/extension-installer": true,
"php-http/discovery": true,
"tbachert/spi": true
}
},
"extra": {
"laravel": {
"dont-discover": [
"laravel/dusk"
]
},
"metasyntactical/composer-plugin-license-check": {
"whitelist": [],
"blacklist": [
"AGPL"
]
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/PHP-FFMpeg/BinaryDriver.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-salesloft.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-aircall.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-pipedrive.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-ringcentral"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-dialpad.git"
}
],
"prefer-stable": true
}
Install
Update
Show log
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.796875,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"bounds":{"left":0.8121675,"top":0.019952115,"width":0.103390954,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.4893617,"top":0.19952115,"width":0.009640957,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.50099736,"top":0.19952115,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5106383,"top":0.19792499,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.51795214,"top":0.19792499,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Providers;\n\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\RateLimiter;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Routing\\Router;\nuse Illuminate\\Foundation\\Support\\Providers\\RouteServiceProvider as ServiceProvider;\nuse Illuminate\\Support\\Facades\\Route;\nuse Jiminny\\Component\\DealRisks\\DealRisk;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Controllers\\CustomerApi\\CustomerApiController;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Ai\\AiScorecard;\nuse Jiminny\\Models\\Ai\\AiScorecardRule;\nuse Jiminny\\Models\\Ai\\CrmTemplate;\nuse Jiminny\\Models\\Ai\\CrmTemplateField;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Invitation;\nuse Jiminny\\Models\\JobTitle;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Models\\Session;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\n\nclass RouteServiceProvider extends ServiceProvider\n{\n /**\n * Define your route model bindings, pattern filters, etc.\n */\n public function boot(): void\n {\n $this->configureRateLimiting();\n\n $this->routes(function (Router $router) {\n $this\n ->mapHealthRoutes($router)\n ->mapWebhookRoutes($router)\n ->mapApiRoutes($router)\n ->mapApiV2Routes($router)\n ->mapWebRoutes($router)\n ->mapScimRoutes($router)\n ->mapCustomerApiRoutes($router)\n ->mapEmbeddedRoutes($router)\n ;\n });\n $this->defineRouteBindings();\n }\n\n private function mapHealthRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n ], static function (Router $router) {\n require base_path('routes/health.php');\n });\n\n return $this;\n }\n\n /**\n * Define the route model bindings.\n */\n protected function defineRouteBindings()\n {\n Route::pattern('teamSlug', '^[.\\-_a-z0-9]*$');\n Route::pattern('userSlug', '^[.\\-_a-z0-9]*$');\n\n Route::bind('participant', function ($value) {\n return Participant::uuid($value);\n });\n\n Route::bind('participantA', static fn (string $value): Participant => Participant::uuid($value));\n\n Route::bind('participantB', static fn (string $value): Participant => Participant::uuid($value));\n\n Route::bind('connection', function ($value) {\n return Connection::uuid($value);\n });\n\n Route::bind('activity', function ($value) {\n return Activity::uuid($value);\n });\n\n Route::bind('session', function (string $value) {\n return Session::uuid($value);\n });\n\n Route::bind('transcription', function ($value) {\n return Activity\\Transcription::uuid($value);\n });\n\n Route::bind('transcriptionProvider', static function (string $scorecardUuid): Models\\TranscriptionProvider {\n return Models\\TranscriptionProvider::where(\n 'uuid',\n Models\\TranscriptionProvider::toOptimized($scorecardUuid)\n )->firstOrFail();\n });\n\n Route::bind('coachingFeedback', function ($value) {\n return CoachingFeedback::uuid($value);\n });\n\n Route::bind('team', function ($value) {\n return Team::uuid($value);\n });\n\n Route::bind('crmTemplate', fn ($value) => CrmTemplate::uuid($value));\n\n Route::bind('crmTemplateField', fn ($value) => CrmTemplateField::uuid($value));\n\n Route::bind('aiScorecard', fn ($value) => AiScorecard::uuid($value));\n\n Route::bind('aiScorecardRule', fn ($value) => AiScorecardRule::uuid($value));\n\n Route::bind('group', function ($value) {\n return Group::uuid($value);\n });\n\n Route::bind('user', function ($value) {\n return User::uuid($value);\n });\n\n Route::bind('comment', function (string $value, \\Illuminate\\Routing\\Route $route) {\n $targetController = $route->getController();\n if (! $targetController instanceof CommentContextInterface) {\n throw new LogicException(\n 'Type hinting comment will require additional effort due to polymorphism.'\n . ' Either implement CommentContextInterface or inject $commentId and process manually',\n );\n }\n\n $commentClass = $targetController::getCommentImplementation();\n\n return $commentClass::uuid($value);\n });\n\n Route::bind('track', function ($value) {\n return Track::uuid($value);\n });\n\n Route::bind('invitation', function ($value) {\n return Invitation::uuid($value);\n });\n\n Route::bind('playbook', function ($value) {\n return Playbook::uuid($value);\n });\n\n Route::bind('category', function ($value) {\n return PlaybookCategory::uuid($value);\n });\n\n Route::bind('coachingSection', function ($value) {\n return CoachingSection::uuid($value);\n });\n\n Route::bind('coachingSectionCriterion', function ($value) {\n return Models\\CoachingSectionCriterion::uuid($value);\n });\n\n Route::bind('playlist', function ($value) {\n return Playlist::uuid($value);\n });\n\n Route::bind('playlistActivity', static fn (string $value) => PlaylistActivity::uuid($value));\n\n Route::bind('playlistTrack', static fn (string $value) => PlaylistActivity::uuid($value));\n\n Route::bind('playlistShare', static fn (string $uuid) => Playlist\\Share::uuid($uuid));\n\n Route::bind('job', function ($value) {\n return JobTitle::uuid($value);\n });\n\n Route::bind('notification', function ($value) {\n return Activity\\AvailabilityNotification::uuid($value);\n });\n\n Route::bind('activityMoment', function ($value) {\n return Activity\\Moment::uuid($value);\n });\n\n Route::bind('search', function ($value) {\n return Activity\\Search::uuid($value);\n });\n\n Route::bind('nudge', static function ($value): Nudge {\n return Nudge::uuid($value);\n });\n\n Route::bind('theme', static function ($value): Models\\PlaybackTheme {\n return Models\\PlaybackTheme::uuid($value);\n });\n\n Route::bind('topic', static function ($value): Models\\PlaybackTheme\\Topic {\n return Models\\PlaybackTheme\\Topic::uuid($value);\n });\n\n Route::bind('topicTrigger', static function ($value): Models\\PlaybackTheme\\TopicTrigger {\n return Models\\PlaybackTheme\\TopicTrigger::uuid($value);\n });\n\n Route::bind('vocabulary', static function (string $id): Models\\Vocabulary {\n return Models\\Vocabulary::uuid($id);\n });\n\n Route::bind('teamDomain', function ($value) {\n return Models\\TeamDomain::uuid($value);\n });\n\n Route::bind('opportunity', function ($value) {\n return Opportunity::uuid($value);\n });\n\n Route::bind('dealRisk', function ($value) {\n return DealRisk::uuid($value);\n });\n\n Route::bind('layout', static fn (string $uuid) => Models\\Crm\\Layout::uuid($uuid));\n\n Route::bind('scorecard', static function (string $scorecardUuid): Models\\Scorecard\\Scorecard {\n return Models\\Scorecard\\Scorecard::where(\n 'uuid',\n Models\\Scorecard\\Scorecard::toOptimized($scorecardUuid)\n )->firstOrFail();\n });\n\n Route::bind('scorecardRule', static function (string $scorecardRuleUuid): Models\\Scorecard\\ScorecardRule {\n return Models\\Scorecard\\ScorecardRule::where(\n 'uuid',\n Models\\Scorecard\\ScorecardRule::toOptimized($scorecardRuleUuid)\n )->firstOrFail();\n });\n\n Route::bind('askAnythingPrompt', function ($value) {\n return Models\\AskAnything\\AskAnythingPrompt::uuid($value);\n });\n }\n\n /**\n * Define the routes for the application.\n *\n * @param \\Illuminate\\Routing\\Router $router\n */\n\n /**\n * Define the \"web\" routes for the application.\n *\n * These routes all receive session state, CSRF protection, etc.\n */\n private function mapWebRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['web'],\n ], static function (Router $router) {\n require base_path('routes/web.php');\n });\n\n return $this;\n }\n\n /**\n * Define the \"Embedded\" routes for the application.\n *\n * These routes depend on a Partitioned cookie,\n * and are accessible only after login.\n */\n private function mapEmbeddedRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['embedded'],\n 'prefix' => 'embedded',\n ], static function (Router $router) {\n require base_path('routes/embedded.php');\n });\n\n return $this;\n }\n\n private function mapWebhookRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['webhook'],\n 'prefix' => 'webhook',\n ], function (Router $router) {\n require base_path('routes/webhook.php');\n });\n\n return $this;\n }\n\n /**\n * Define the \"api\" routes for the application.\n */\n private function mapApiRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['api'],\n 'prefix' => 'api/v1',\n ], function (Router $router) {\n require base_path('routes/api.php');\n });\n\n return $this;\n }\n\n private function mapApiV2Routes(Router $router): self\n {\n $router->group([\n 'middleware' => ['api', 'auth:api'],\n 'prefix' => 'api/v2',\n ], static function (Router $router) {\n require base_path('routes/api_v2.php');\n });\n\n return $this;\n }\n\n private function mapScimRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['scim'],\n 'prefix' => 'scim/v2',\n ], function (Router $router) {\n require base_path('routes/scim.php');\n });\n\n return $this;\n }\n\n private function mapCustomerApiRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['customer-api'],\n 'prefix' => 'customer/api/v1',\n ], function (Router $router) {\n require base_path('routes/customer_api.php');\n });\n\n return $this;\n }\n\n /** Configure the rate limiters for the application. */\n protected function configureRateLimiting(): void\n {\n RateLimiter::for('api', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User || $user instanceof Models\\Partner) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n if (! is_string($subject)) {\n throw new LogicException('Unable to identify subject to rate limit');\n }\n\n return Limit::perMinute(120)->by($subject);\n });\n\n RateLimiter::for('customer-api', static function (Request $request) {\n // Get the token using the same method as the middleware\n $token = null;\n if ($request->bearerToken() !== null) {\n $token = $request->bearerToken();\n } elseif ($request->get('api_token') !== null) {\n $token = $request->get('api_token');\n } elseif ($request->post('api_token') !== null) {\n $token = $request->post('api_token');\n }\n\n // If we have a valid token, get the team ID for rate limiting\n if ($token !== null && strlen($token) === Team::CUSTOMER_API_TOKEN_LENGTH) {\n $tokens = Cache::remember(Team::CUSTOMER_API_TOKENS_CACHE, 60 * 60, static function () {\n return Team::query()->whereNotNull('api_token')->pluck('id', 'api_token');\n });\n\n $hashedToken = hash('sha256', $token);\n if (is_countable($tokens) && ! empty($tokens) && isset($tokens[$hashedToken])) {\n $teamId = $tokens[$hashedToken];\n $team = Team::find($teamId);\n\n if ($team) {\n $subject = $team->getUuid();\n\n // Special rate limit for Funding Circle and Cision.\n if (in_array($subject, [\n '9762611a-fc86-4234-baba-a436de26e774', '1151b5b0-0680-4190-b30c-72649280837d',\n '16b50903-a115-448f-877c-6c01e8b45f5d', 'bfc1c304-83b2-47a3-a7ce-1d28b0e1e90e',\n '447ee473-7dd8-41bc-a702-9322e5b0ec5b'])) {\n // Route-specific rate limiting\n if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {\n return Limit::perMinute(40)->by($subject);\n }\n\n return Limit::perMinute(1000)->by($subject);\n }\n\n // Route-specific rate limiting\n if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {\n return Limit::perMinute(30)->by($subject);\n }\n\n return Limit::perMinute(120)->by($subject);\n }\n }\n }\n\n // Default fallback to IP-based limiting\n return Limit::perMinute(120)->by($request->ip());\n });\n\n RateLimiter::for('conference-consent', static function (Request $request) {\n // Rate limit by IP address for public consent endpoint\n // Allow 10 requests per minute per IP to prevent abuse while allowing legitimate use\n return Limit::perMinute(10)->by($request->ip());\n });\n\n RateLimiter::for('activity-export-shareable-link', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(30)->by($subject);\n });\n\n RateLimiter::for('activity-export', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(10)->by($subject);\n });\n\n RateLimiter::for('transcription-download', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(3)->by($subject);\n });\n }\n}","depth":4,"value":"<?php\n\nnamespace Jiminny\\Providers;\n\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\RateLimiter;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Routing\\Router;\nuse Illuminate\\Foundation\\Support\\Providers\\RouteServiceProvider as ServiceProvider;\nuse Illuminate\\Support\\Facades\\Route;\nuse Jiminny\\Component\\DealRisks\\DealRisk;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Controllers\\CustomerApi\\CustomerApiController;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Ai\\AiScorecard;\nuse Jiminny\\Models\\Ai\\AiScorecardRule;\nuse Jiminny\\Models\\Ai\\CrmTemplate;\nuse Jiminny\\Models\\Ai\\CrmTemplateField;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Invitation;\nuse Jiminny\\Models\\JobTitle;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Models\\Session;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\n\nclass RouteServiceProvider extends ServiceProvider\n{\n /**\n * Define your route model bindings, pattern filters, etc.\n */\n public function boot(): void\n {\n $this->configureRateLimiting();\n\n $this->routes(function (Router $router) {\n $this\n ->mapHealthRoutes($router)\n ->mapWebhookRoutes($router)\n ->mapApiRoutes($router)\n ->mapApiV2Routes($router)\n ->mapWebRoutes($router)\n ->mapScimRoutes($router)\n ->mapCustomerApiRoutes($router)\n ->mapEmbeddedRoutes($router)\n ;\n });\n $this->defineRouteBindings();\n }\n\n private function mapHealthRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n ], static function (Router $router) {\n require base_path('routes/health.php');\n });\n\n return $this;\n }\n\n /**\n * Define the route model bindings.\n */\n protected function defineRouteBindings()\n {\n Route::pattern('teamSlug', '^[.\\-_a-z0-9]*$');\n Route::pattern('userSlug', '^[.\\-_a-z0-9]*$');\n\n Route::bind('participant', function ($value) {\n return Participant::uuid($value);\n });\n\n Route::bind('participantA', static fn (string $value): Participant => Participant::uuid($value));\n\n Route::bind('participantB', static fn (string $value): Participant => Participant::uuid($value));\n\n Route::bind('connection', function ($value) {\n return Connection::uuid($value);\n });\n\n Route::bind('activity', function ($value) {\n return Activity::uuid($value);\n });\n\n Route::bind('session', function (string $value) {\n return Session::uuid($value);\n });\n\n Route::bind('transcription', function ($value) {\n return Activity\\Transcription::uuid($value);\n });\n\n Route::bind('transcriptionProvider', static function (string $scorecardUuid): Models\\TranscriptionProvider {\n return Models\\TranscriptionProvider::where(\n 'uuid',\n Models\\TranscriptionProvider::toOptimized($scorecardUuid)\n )->firstOrFail();\n });\n\n Route::bind('coachingFeedback', function ($value) {\n return CoachingFeedback::uuid($value);\n });\n\n Route::bind('team', function ($value) {\n return Team::uuid($value);\n });\n\n Route::bind('crmTemplate', fn ($value) => CrmTemplate::uuid($value));\n\n Route::bind('crmTemplateField', fn ($value) => CrmTemplateField::uuid($value));\n\n Route::bind('aiScorecard', fn ($value) => AiScorecard::uuid($value));\n\n Route::bind('aiScorecardRule', fn ($value) => AiScorecardRule::uuid($value));\n\n Route::bind('group', function ($value) {\n return Group::uuid($value);\n });\n\n Route::bind('user', function ($value) {\n return User::uuid($value);\n });\n\n Route::bind('comment', function (string $value, \\Illuminate\\Routing\\Route $route) {\n $targetController = $route->getController();\n if (! $targetController instanceof CommentContextInterface) {\n throw new LogicException(\n 'Type hinting comment will require additional effort due to polymorphism.'\n . ' Either implement CommentContextInterface or inject $commentId and process manually',\n );\n }\n\n $commentClass = $targetController::getCommentImplementation();\n\n return $commentClass::uuid($value);\n });\n\n Route::bind('track', function ($value) {\n return Track::uuid($value);\n });\n\n Route::bind('invitation', function ($value) {\n return Invitation::uuid($value);\n });\n\n Route::bind('playbook', function ($value) {\n return Playbook::uuid($value);\n });\n\n Route::bind('category', function ($value) {\n return PlaybookCategory::uuid($value);\n });\n\n Route::bind('coachingSection', function ($value) {\n return CoachingSection::uuid($value);\n });\n\n Route::bind('coachingSectionCriterion', function ($value) {\n return Models\\CoachingSectionCriterion::uuid($value);\n });\n\n Route::bind('playlist', function ($value) {\n return Playlist::uuid($value);\n });\n\n Route::bind('playlistActivity', static fn (string $value) => PlaylistActivity::uuid($value));\n\n Route::bind('playlistTrack', static fn (string $value) => PlaylistActivity::uuid($value));\n\n Route::bind('playlistShare', static fn (string $uuid) => Playlist\\Share::uuid($uuid));\n\n Route::bind('job', function ($value) {\n return JobTitle::uuid($value);\n });\n\n Route::bind('notification', function ($value) {\n return Activity\\AvailabilityNotification::uuid($value);\n });\n\n Route::bind('activityMoment', function ($value) {\n return Activity\\Moment::uuid($value);\n });\n\n Route::bind('search', function ($value) {\n return Activity\\Search::uuid($value);\n });\n\n Route::bind('nudge', static function ($value): Nudge {\n return Nudge::uuid($value);\n });\n\n Route::bind('theme', static function ($value): Models\\PlaybackTheme {\n return Models\\PlaybackTheme::uuid($value);\n });\n\n Route::bind('topic', static function ($value): Models\\PlaybackTheme\\Topic {\n return Models\\PlaybackTheme\\Topic::uuid($value);\n });\n\n Route::bind('topicTrigger', static function ($value): Models\\PlaybackTheme\\TopicTrigger {\n return Models\\PlaybackTheme\\TopicTrigger::uuid($value);\n });\n\n Route::bind('vocabulary', static function (string $id): Models\\Vocabulary {\n return Models\\Vocabulary::uuid($id);\n });\n\n Route::bind('teamDomain', function ($value) {\n return Models\\TeamDomain::uuid($value);\n });\n\n Route::bind('opportunity', function ($value) {\n return Opportunity::uuid($value);\n });\n\n Route::bind('dealRisk', function ($value) {\n return DealRisk::uuid($value);\n });\n\n Route::bind('layout', static fn (string $uuid) => Models\\Crm\\Layout::uuid($uuid));\n\n Route::bind('scorecard', static function (string $scorecardUuid): Models\\Scorecard\\Scorecard {\n return Models\\Scorecard\\Scorecard::where(\n 'uuid',\n Models\\Scorecard\\Scorecard::toOptimized($scorecardUuid)\n )->firstOrFail();\n });\n\n Route::bind('scorecardRule', static function (string $scorecardRuleUuid): Models\\Scorecard\\ScorecardRule {\n return Models\\Scorecard\\ScorecardRule::where(\n 'uuid',\n Models\\Scorecard\\ScorecardRule::toOptimized($scorecardRuleUuid)\n )->firstOrFail();\n });\n\n Route::bind('askAnythingPrompt', function ($value) {\n return Models\\AskAnything\\AskAnythingPrompt::uuid($value);\n });\n }\n\n /**\n * Define the routes for the application.\n *\n * @param \\Illuminate\\Routing\\Router $router\n */\n\n /**\n * Define the \"web\" routes for the application.\n *\n * These routes all receive session state, CSRF protection, etc.\n */\n private function mapWebRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['web'],\n ], static function (Router $router) {\n require base_path('routes/web.php');\n });\n\n return $this;\n }\n\n /**\n * Define the \"Embedded\" routes for the application.\n *\n * These routes depend on a Partitioned cookie,\n * and are accessible only after login.\n */\n private function mapEmbeddedRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['embedded'],\n 'prefix' => 'embedded',\n ], static function (Router $router) {\n require base_path('routes/embedded.php');\n });\n\n return $this;\n }\n\n private function mapWebhookRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['webhook'],\n 'prefix' => 'webhook',\n ], function (Router $router) {\n require base_path('routes/webhook.php');\n });\n\n return $this;\n }\n\n /**\n * Define the \"api\" routes for the application.\n */\n private function mapApiRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['api'],\n 'prefix' => 'api/v1',\n ], function (Router $router) {\n require base_path('routes/api.php');\n });\n\n return $this;\n }\n\n private function mapApiV2Routes(Router $router): self\n {\n $router->group([\n 'middleware' => ['api', 'auth:api'],\n 'prefix' => 'api/v2',\n ], static function (Router $router) {\n require base_path('routes/api_v2.php');\n });\n\n return $this;\n }\n\n private function mapScimRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['scim'],\n 'prefix' => 'scim/v2',\n ], function (Router $router) {\n require base_path('routes/scim.php');\n });\n\n return $this;\n }\n\n private function mapCustomerApiRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['customer-api'],\n 'prefix' => 'customer/api/v1',\n ], function (Router $router) {\n require base_path('routes/customer_api.php');\n });\n\n return $this;\n }\n\n /** Configure the rate limiters for the application. */\n protected function configureRateLimiting(): void\n {\n RateLimiter::for('api', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User || $user instanceof Models\\Partner) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n if (! is_string($subject)) {\n throw new LogicException('Unable to identify subject to rate limit');\n }\n\n return Limit::perMinute(120)->by($subject);\n });\n\n RateLimiter::for('customer-api', static function (Request $request) {\n // Get the token using the same method as the middleware\n $token = null;\n if ($request->bearerToken() !== null) {\n $token = $request->bearerToken();\n } elseif ($request->get('api_token') !== null) {\n $token = $request->get('api_token');\n } elseif ($request->post('api_token') !== null) {\n $token = $request->post('api_token');\n }\n\n // If we have a valid token, get the team ID for rate limiting\n if ($token !== null && strlen($token) === Team::CUSTOMER_API_TOKEN_LENGTH) {\n $tokens = Cache::remember(Team::CUSTOMER_API_TOKENS_CACHE, 60 * 60, static function () {\n return Team::query()->whereNotNull('api_token')->pluck('id', 'api_token');\n });\n\n $hashedToken = hash('sha256', $token);\n if (is_countable($tokens) && ! empty($tokens) && isset($tokens[$hashedToken])) {\n $teamId = $tokens[$hashedToken];\n $team = Team::find($teamId);\n\n if ($team) {\n $subject = $team->getUuid();\n\n // Special rate limit for Funding Circle and Cision.\n if (in_array($subject, [\n '9762611a-fc86-4234-baba-a436de26e774', '1151b5b0-0680-4190-b30c-72649280837d',\n '16b50903-a115-448f-877c-6c01e8b45f5d', 'bfc1c304-83b2-47a3-a7ce-1d28b0e1e90e',\n '447ee473-7dd8-41bc-a702-9322e5b0ec5b'])) {\n // Route-specific rate limiting\n if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {\n return Limit::perMinute(40)->by($subject);\n }\n\n return Limit::perMinute(1000)->by($subject);\n }\n\n // Route-specific rate limiting\n if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {\n return Limit::perMinute(30)->by($subject);\n }\n\n return Limit::perMinute(120)->by($subject);\n }\n }\n }\n\n // Default fallback to IP-based limiting\n return Limit::perMinute(120)->by($request->ip());\n });\n\n RateLimiter::for('conference-consent', static function (Request $request) {\n // Rate limit by IP address for public consent endpoint\n // Allow 10 requests per minute per IP to prevent abuse while allowing legitimate use\n return Limit::perMinute(10)->by($request->ip());\n });\n\n RateLimiter::for('activity-export-shareable-link', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(30)->by($subject);\n });\n\n RateLimiter::for('activity-export', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(10)->by($subject);\n });\n\n RateLimiter::for('transcription-download', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(3)->by($subject);\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.9527925,"top":0.10055866,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.96276593,"top":0.10055866,"width":0.009640957,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"{\n \"name\": \"jiminny/app\",\n \"description\": \"The Jiminny Platform.\",\n \"keywords\": [\n \"training\",\n \"salesforce\",\n \"conference\"\n ],\n \"license\": \"MIT\",\n \"type\": \"project\",\n \"require\": {\n \"php\": \"^8.3\",\n \"ext-ctype\": \"*\",\n \"ext-curl\": \"*\",\n \"ext-date\": \"*\",\n \"ext-dom\": \"*\",\n \"ext-fileinfo\": \"*\",\n \"ext-filter\": \"*\",\n \"ext-gd\": \"*\",\n \"ext-gmp\": \"*\",\n \"ext-hash\": \"*\",\n \"ext-iconv\": \"*\",\n \"ext-igbinary\": \"*\",\n \"ext-imagick\": \"*\",\n \"ext-intl\": \"*\",\n \"ext-json\": \"*\",\n \"ext-libxml\": \"*\",\n \"ext-mailparse\": \"*\",\n \"ext-mbstring\": \"*\",\n \"ext-mysqlnd\": \"*\",\n \"ext-openssl\": \"*\",\n \"ext-pcntl\": \"*\",\n \"ext-pcre\": \"*\",\n \"ext-pdo\": \"*\",\n \"ext-pdo_mysql\": \"*\",\n \"ext-phar\": \"*\",\n \"ext-phpiredis\": \"*\",\n \"ext-posix\": \"*\",\n \"ext-readline\": \"*\",\n \"ext-redis\": \"*\",\n \"ext-reflection\": \"*\",\n \"ext-session\": \"*\",\n \"ext-simplexml\": \"*\",\n \"ext-sockets\": \"*\",\n \"ext-spl\": \"*\",\n \"ext-tokenizer\": \"*\",\n \"ext-xml\": \"*\",\n \"ext-xmlreader\": \"*\",\n \"ext-xmlwriter\": \"*\",\n \"ext-zend-opcache\": \"*\",\n \"ext-zip\": \"*\",\n \"ext-zlib\": \"*\",\n \"lib-curl\": \"*\",\n \"lib-curl-openssl\": \"*\",\n \"lib-curl-zlib\": \"*\",\n \"lib-date-timelib\": \"*\",\n \"lib-date-zoneinfo\": \"*\",\n \"lib-fileinfo-libmagic\": \"*\",\n \"lib-gd\": \"*\",\n \"lib-gd-freetype\": \"*\",\n \"lib-gd-libjpeg\": \"*\",\n \"lib-gd-libpng\": \"*\",\n \"lib-gmp\": \"*\",\n \"lib-icu\": \"*\",\n \"lib-icu-cldr\": \"*\",\n \"lib-icu-unicode\": \"*\",\n \"lib-imagick-imagemagick\": \"*\",\n \"lib-libxml\": \"*\",\n \"lib-mbstring-libmbfl\": \"*\",\n \"lib-mbstring-oniguruma\": \"*\",\n \"lib-openssl\": \"*\",\n \"lib-pcre\": \"*\",\n \"lib-pcre-unicode\": \"*\",\n \"lib-zip-libzip\": \"*\",\n \"lib-zlib\": \"*\",\n \"24slides/laravel-saml2\": \"^2.4\",\n \"adam-paterson/oauth2-slack\": \"^1.1\",\n \"asimlqt/php-google-spreadsheet-client\": \"^3.0\",\n \"aws/aws-sdk-php\": \"^3.368\",\n \"aws/aws-sdk-php-laravel\": \"^3.10\",\n \"bepsvpt/secure-headers\": \"^9.0\",\n \"chadhutchins/oauth2-slack\": \"^1.2\",\n \"chaseconey/laravel-datadog-helper\": \"^1.2\",\n \"chrisyue/php-m3u8\": \"4.0.3\",\n \"daniti/oauth2-pipedrive\": \"dev-master\",\n \"devio/pipedrive\": \"^2.6\",\n \"doctrine/dbal\": \"^4.0\",\n \"elasticsearch/elasticsearch\": \"^7.11\",\n \"erusev/parsedown\": \"^1.7\",\n \"fakerphp/faker\": \"^1.23\",\n \"firebase/php-jwt\": \"^7.0\",\n \"flipboxdigital/oauth2-hubspot\": \"1.0.1\",\n \"giggsey/libphonenumber-for-php\": \"^8.12\",\n \"google/apiclient\": \"^2.19\",\n \"google/apiclient-services\": \"~0.360\",\n \"google/apps-meet\": \"^0.5.1\",\n \"guzzlehttp/guzzle\": \"^7.8\",\n \"guzzlehttp/psr7\": \"^2.6\",\n \"halaxa/json-machine\": \"^1.2\",\n \"html2text/html2text\": \"^4.3\",\n \"hubspot/api-client\": \"~5.0.0\",\n \"hubspot/hubspot-php\": \"^5.2.0\",\n \"intercom/intercom-php\": \"^4.5\",\n \"intervention/image\": \"^3.4\",\n \"jakeasmith/http_build_url\": \"^1.0\",\n \"jdavidbakr/cloudfront-proxies\": \"^1.7\",\n \"jeremykendall/php-domain-parser\": \"^6.3\",\n \"jiminny/oauth2-aircall\": \"dev-master\",\n \"jiminny/oauth2-bullhorn\": \"^0.2.0\",\n \"jiminny/oauth2-dialpad\": \"dev-master\",\n \"jiminny/oauth2-salesloft\": \"dev-master\",\n \"jolicode/slack-php-api\": \"^4.5.0\",\n \"kalnoy/nestedset\": \"*\",\n \"laravel/framework\": \"^12.28\",\n \"laravel/helpers\": \"^1.7\",\n \"laravel/passport\": \"^13.0\",\n \"laravel/slack-notification-channel\": \"^3.4\",\n \"laravel/tinker\": \"^2.10.1\",\n \"laravel/ui\": \"^4.6\",\n \"laravolt/avatar\": \"^6.1\",\n \"league/flysystem\": \"^3.0\",\n \"league/flysystem-aws-s3-v3\": \"^3.0\",\n \"league/fractal\": \"*\",\n \"league/oauth2-client\": \"^2.7\",\n \"league/oauth2-google\": \"^4.0\",\n \"league/oauth2-linkedin\": \"^5.1\",\n \"league/oauth2-server\": \"^9.2\",\n \"league/statsd\": \"^2.0\",\n \"markrogoyski/math-php\": \"^2.7.0\",\n \"microsoft/microsoft-graph\": \"^2.51\",\n \"monolog/monolog\": \"^3.0\",\n \"nesbot/carbon\": \"^3.8\",\n \"nette/caching\": \"*\",\n \"phlib/sms-length\": \"^2.0\",\n \"php-ffmpeg/php-ffmpeg\": \"^1.2\",\n \"php-http/client-common\": \"^2.7\",\n \"php-http/curl-client\": \"^2.3\",\n \"php-http/httplug\": \"^2.2\",\n \"php-http/message\": \"^1.16\",\n \"phpseclib/phpseclib\": \"^3.0.36\",\n \"propaganistas/laravel-phone\": \"^5.3\",\n \"psr/cache\": \"^3.0\",\n \"psr/http-message\": \"^2.0\",\n \"psr/log\": \"^3.0\",\n \"psr/simple-cache\": \"^3.0\",\n \"pusher/pusher-php-server\": \"7.2.3\",\n \"ramsey/uuid\": \"^4.2\",\n \"ringcentral/ringcentral-php\": \"3.0.0\",\n \"rmccue/requests\": \"^2.0\",\n \"ruflin/elastica\": \"^7.1.1\",\n \"santigarcor/laratrust\": \"^8.4\",\n \"sentry/sentry\": \"4.13.0\",\n \"sentry/sentry-laravel\": \"~4.13.0\",\n \"shiftonelabs/laravel-sqs-fifo-queue\": \"^3.0\",\n \"spatie/fractalistic\": \"^2.9\",\n \"spatie/laravel-fractal\": \"^6.3\",\n \"spatie/laravel-ignition\": \"^2.9\",\n \"spatie/laravel-webhook-server\": \"^3.8\",\n \"staudenmeir/belongs-to-through\": \"^2.17\",\n \"stevenmaguire/oauth2-salesforce\": \"^2.0\",\n \"symfony/cache\": \"^7.2\",\n \"symfony/console\": \"^7.2\",\n \"symfony/css-selector\": \"^7.2\",\n \"symfony/debug\": \"^4.4\",\n \"symfony/dom-crawler\": \"^7.2\",\n \"symfony/expression-language\": \"^7.2\",\n \"symfony/finder\": \"^7.2\",\n \"symfony/http-client\": \"^7.3\",\n \"symfony/http-foundation\": \"^7.2\",\n \"symfony/http-kernel\": \"^7.2\",\n \"symfony/postmark-mailer\": \"^7.3\",\n \"symfony/process\": \"^7.3\",\n \"symfony/property-access\": \"^7.2\",\n \"symfony/psr-http-message-bridge\": \"^7.0\",\n \"symfony/var-dumper\": \"^7.2\",\n \"symfony/workflow\": \"^7.2\",\n \"tecnickcom/tcpdf\": \"^6.11\",\n \"thenetworg/oauth2-azure\": \"dev-master\",\n \"tmannherz/oauth2-ringcentral\": \"dev-master\",\n \"twilio/sdk\": \"^8.3\",\n \"vanderlee/php-sentence\": \"^1.0\",\n \"vinkla/hashids\": \"^13.0\",\n \"vlucas/phpdotenv\": \"^5.4\",\n \"wildbit/postmark-php\": \"^6.0\",\n \"willdurand/email-reply-parser\": \"^2.8\",\n \"zbateson/mail-mime-parser\": \"^3.0.4\"\n },\n \"require-dev\": {\n \"barryvdh/laravel-debugbar\": \"^3.15\",\n \"barryvdh/laravel-ide-helper\": \"^3.5\",\n \"brianium/paratest\": \"^7.5\",\n \"browserstack/browserstack-local\": \"^1.1.0\",\n \"filp/whoops\": \"^2.9\",\n \"friendsofphp/php-cs-fixer\": \"^3.66\",\n \"infection/infection\": \"^0.29.14\",\n \"jasonmccreary/laravel-test-assertions\": \"^2.5\",\n \"larastan/larastan\": \"^3.1\",\n \"maglnet/composer-require-checker\": \"^4.8\",\n \"mockery/mockery\": \"^1.6\",\n \"nunomaduro/collision\": \"^8.6\",\n \"phpstan/phpstan\": \"^2.1\",\n \"phpunit/phpunit\": \"^11.5.50\",\n \"symfony/phpunit-bridge\": \"^7.0\",\n \"vimeo/psalm\": \"^6.5.0\"\n },\n \"autoload\": {\n \"classmap\": [\n \"database\"\n ],\n \"psr-4\": {\n \"Jiminny\\\\\": \"app/\",\n \"Tests\\\\\": \"tests/\",\n \"Database\\\\Factories\\\\\": \"database/factories/\",\n \"Database\\\\Seeders\\\\\": \"database/seeders/\",\n \"Microsoft\\\\Graph\\\\Generated\\\\Models\\\\\": \"app/Services/MeetingGenerator/Overrides/Microsoft/Graph/Generated/Models/\"\n },\n \"files\": [\n \"app/helpers.php\"\n ]\n },\n \"autoload-dev\": {\n \"classmap\": [\n \"tests/TestCase.php\"\n ],\n \"psr-4\": {\n \"Jiminny\\\\\": \"app/\",\n \"Tests\\\\\": \"tests/\"\n }\n },\n \"scripts\": {\n \"post-root-package-install\": [\n \"php -r \\\"file_exists('.env') || copy('.env.example', '.env');\\\"\"\n ],\n \"post-create-project-cmd\": [\n \"php artisan key:generate --ansi\"\n ],\n \"post-install-cmd\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postInstall\"\n ],\n \"post-update-cmd\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postUpdate\",\n \"php artisan ide-helper:generate\",\n \"php artisan ide-helper:meta\",\n \"@php artisan vendor:publish --tag=laravel-assets --ansi --force\"\n ],\n \"post-autoload-dump\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postAutoloadDump\",\n \"@php artisan package:discover --ansi\"\n ]\n },\n \"config\": {\n \"preferred-install\": \"dist\",\n \"sort-packages\": true,\n \"optimize-autoloader\": true,\n \"allow-plugins\": {\n \"infection/extension-installer\": true,\n \"php-http/discovery\": true,\n \"tbachert/spi\": true\n }\n },\n \"extra\": {\n \"laravel\": {\n \"dont-discover\": [\n \"laravel/dusk\"\n ]\n },\n \"metasyntactical/composer-plugin-license-check\": {\n \"whitelist\": [],\n \"blacklist\": [\n \"AGPL\"\n ]\n }\n },\n \"repositories\": [\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/PHP-FFMpeg/BinaryDriver.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-salesloft.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-aircall.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-pipedrive.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-ringcentral\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-dialpad.git\"\n }\n ],\n \"prefer-stable\": true\n}","depth":4,"value":"{\n \"name\": \"jiminny/app\",\n \"description\": \"The Jiminny Platform.\",\n \"keywords\": [\n \"training\",\n \"salesforce\",\n \"conference\"\n ],\n \"license\": \"MIT\",\n \"type\": \"project\",\n \"require\": {\n \"php\": \"^8.3\",\n \"ext-ctype\": \"*\",\n \"ext-curl\": \"*\",\n \"ext-date\": \"*\",\n \"ext-dom\": \"*\",\n \"ext-fileinfo\": \"*\",\n \"ext-filter\": \"*\",\n \"ext-gd\": \"*\",\n \"ext-gmp\": \"*\",\n \"ext-hash\": \"*\",\n \"ext-iconv\": \"*\",\n \"ext-igbinary\": \"*\",\n \"ext-imagick\": \"*\",\n \"ext-intl\": \"*\",\n \"ext-json\": \"*\",\n \"ext-libxml\": \"*\",\n \"ext-mailparse\": \"*\",\n \"ext-mbstring\": \"*\",\n \"ext-mysqlnd\": \"*\",\n \"ext-openssl\": \"*\",\n \"ext-pcntl\": \"*\",\n \"ext-pcre\": \"*\",\n \"ext-pdo\": \"*\",\n \"ext-pdo_mysql\": \"*\",\n \"ext-phar\": \"*\",\n \"ext-phpiredis\": \"*\",\n \"ext-posix\": \"*\",\n \"ext-readline\": \"*\",\n \"ext-redis\": \"*\",\n \"ext-reflection\": \"*\",\n \"ext-session\": \"*\",\n \"ext-simplexml\": \"*\",\n \"ext-sockets\": \"*\",\n \"ext-spl\": \"*\",\n \"ext-tokenizer\": \"*\",\n \"ext-xml\": \"*\",\n \"ext-xmlreader\": \"*\",\n \"ext-xmlwriter\": \"*\",\n \"ext-zend-opcache\": \"*\",\n \"ext-zip\": \"*\",\n \"ext-zlib\": \"*\",\n \"lib-curl\": \"*\",\n \"lib-curl-openssl\": \"*\",\n \"lib-curl-zlib\": \"*\",\n \"lib-date-timelib\": \"*\",\n \"lib-date-zoneinfo\": \"*\",\n \"lib-fileinfo-libmagic\": \"*\",\n \"lib-gd\": \"*\",\n \"lib-gd-freetype\": \"*\",\n \"lib-gd-libjpeg\": \"*\",\n \"lib-gd-libpng\": \"*\",\n \"lib-gmp\": \"*\",\n \"lib-icu\": \"*\",\n \"lib-icu-cldr\": \"*\",\n \"lib-icu-unicode\": \"*\",\n \"lib-imagick-imagemagick\": \"*\",\n \"lib-libxml\": \"*\",\n \"lib-mbstring-libmbfl\": \"*\",\n \"lib-mbstring-oniguruma\": \"*\",\n \"lib-openssl\": \"*\",\n \"lib-pcre\": \"*\",\n \"lib-pcre-unicode\": \"*\",\n \"lib-zip-libzip\": \"*\",\n \"lib-zlib\": \"*\",\n \"24slides/laravel-saml2\": \"^2.4\",\n \"adam-paterson/oauth2-slack\": \"^1.1\",\n \"asimlqt/php-google-spreadsheet-client\": \"^3.0\",\n \"aws/aws-sdk-php\": \"^3.368\",\n \"aws/aws-sdk-php-laravel\": \"^3.10\",\n \"bepsvpt/secure-headers\": \"^9.0\",\n \"chadhutchins/oauth2-slack\": \"^1.2\",\n \"chaseconey/laravel-datadog-helper\": \"^1.2\",\n \"chrisyue/php-m3u8\": \"4.0.3\",\n \"daniti/oauth2-pipedrive\": \"dev-master\",\n \"devio/pipedrive\": \"^2.6\",\n \"doctrine/dbal\": \"^4.0\",\n \"elasticsearch/elasticsearch\": \"^7.11\",\n \"erusev/parsedown\": \"^1.7\",\n \"fakerphp/faker\": \"^1.23\",\n \"firebase/php-jwt\": \"^7.0\",\n \"flipboxdigital/oauth2-hubspot\": \"1.0.1\",\n \"giggsey/libphonenumber-for-php\": \"^8.12\",\n \"google/apiclient\": \"^2.19\",\n \"google/apiclient-services\": \"~0.360\",\n \"google/apps-meet\": \"^0.5.1\",\n \"guzzlehttp/guzzle\": \"^7.8\",\n \"guzzlehttp/psr7\": \"^2.6\",\n \"halaxa/json-machine\": \"^1.2\",\n \"html2text/html2text\": \"^4.3\",\n \"hubspot/api-client\": \"~5.0.0\",\n \"hubspot/hubspot-php\": \"^5.2.0\",\n \"intercom/intercom-php\": \"^4.5\",\n \"intervention/image\": \"^3.4\",\n \"jakeasmith/http_build_url\": \"^1.0\",\n \"jdavidbakr/cloudfront-proxies\": \"^1.7\",\n \"jeremykendall/php-domain-parser\": \"^6.3\",\n \"jiminny/oauth2-aircall\": \"dev-master\",\n \"jiminny/oauth2-bullhorn\": \"^0.2.0\",\n \"jiminny/oauth2-dialpad\": \"dev-master\",\n \"jiminny/oauth2-salesloft\": \"dev-master\",\n \"jolicode/slack-php-api\": \"^4.5.0\",\n \"kalnoy/nestedset\": \"*\",\n \"laravel/framework\": \"^12.28\",\n \"laravel/helpers\": \"^1.7\",\n \"laravel/passport\": \"^13.0\",\n \"laravel/slack-notification-channel\": \"^3.4\",\n \"laravel/tinker\": \"^2.10.1\",\n \"laravel/ui\": \"^4.6\",\n \"laravolt/avatar\": \"^6.1\",\n \"league/flysystem\": \"^3.0\",\n \"league/flysystem-aws-s3-v3\": \"^3.0\",\n \"league/fractal\": \"*\",\n \"league/oauth2-client\": \"^2.7\",\n \"league/oauth2-google\": \"^4.0\",\n \"league/oauth2-linkedin\": \"^5.1\",\n \"league/oauth2-server\": \"^9.2\",\n \"league/statsd\": \"^2.0\",\n \"markrogoyski/math-php\": \"^2.7.0\",\n \"microsoft/microsoft-graph\": \"^2.51\",\n \"monolog/monolog\": \"^3.0\",\n \"nesbot/carbon\": \"^3.8\",\n \"nette/caching\": \"*\",\n \"phlib/sms-length\": \"^2.0\",\n \"php-ffmpeg/php-ffmpeg\": \"^1.2\",\n \"php-http/client-common\": \"^2.7\",\n \"php-http/curl-client\": \"^2.3\",\n \"php-http/httplug\": \"^2.2\",\n \"php-http/message\": \"^1.16\",\n \"phpseclib/phpseclib\": \"^3.0.36\",\n \"propaganistas/laravel-phone\": \"^5.3\",\n \"psr/cache\": \"^3.0\",\n \"psr/http-message\": \"^2.0\",\n \"psr/log\": \"^3.0\",\n \"psr/simple-cache\": \"^3.0\",\n \"pusher/pusher-php-server\": \"7.2.3\",\n \"ramsey/uuid\": \"^4.2\",\n \"ringcentral/ringcentral-php\": \"3.0.0\",\n \"rmccue/requests\": \"^2.0\",\n \"ruflin/elastica\": \"^7.1.1\",\n \"santigarcor/laratrust\": \"^8.4\",\n \"sentry/sentry\": \"4.13.0\",\n \"sentry/sentry-laravel\": \"~4.13.0\",\n \"shiftonelabs/laravel-sqs-fifo-queue\": \"^3.0\",\n \"spatie/fractalistic\": \"^2.9\",\n \"spatie/laravel-fractal\": \"^6.3\",\n \"spatie/laravel-ignition\": \"^2.9\",\n \"spatie/laravel-webhook-server\": \"^3.8\",\n \"staudenmeir/belongs-to-through\": \"^2.17\",\n \"stevenmaguire/oauth2-salesforce\": \"^2.0\",\n \"symfony/cache\": \"^7.2\",\n \"symfony/console\": \"^7.2\",\n \"symfony/css-selector\": \"^7.2\",\n \"symfony/debug\": \"^4.4\",\n \"symfony/dom-crawler\": \"^7.2\",\n \"symfony/expression-language\": \"^7.2\",\n \"symfony/finder\": \"^7.2\",\n \"symfony/http-client\": \"^7.3\",\n \"symfony/http-foundation\": \"^7.2\",\n \"symfony/http-kernel\": \"^7.2\",\n \"symfony/postmark-mailer\": \"^7.3\",\n \"symfony/process\": \"^7.3\",\n \"symfony/property-access\": \"^7.2\",\n \"symfony/psr-http-message-bridge\": \"^7.0\",\n \"symfony/var-dumper\": \"^7.2\",\n \"symfony/workflow\": \"^7.2\",\n \"tecnickcom/tcpdf\": \"^6.11\",\n \"thenetworg/oauth2-azure\": \"dev-master\",\n \"tmannherz/oauth2-ringcentral\": \"dev-master\",\n \"twilio/sdk\": \"^8.3\",\n \"vanderlee/php-sentence\": \"^1.0\",\n \"vinkla/hashids\": \"^13.0\",\n \"vlucas/phpdotenv\": \"^5.4\",\n \"wildbit/postmark-php\": \"^6.0\",\n \"willdurand/email-reply-parser\": \"^2.8\",\n \"zbateson/mail-mime-parser\": \"^3.0.4\"\n },\n \"require-dev\": {\n \"barryvdh/laravel-debugbar\": \"^3.15\",\n \"barryvdh/laravel-ide-helper\": \"^3.5\",\n \"brianium/paratest\": \"^7.5\",\n \"browserstack/browserstack-local\": \"^1.1.0\",\n \"filp/whoops\": \"^2.9\",\n \"friendsofphp/php-cs-fixer\": \"^3.66\",\n \"infection/infection\": \"^0.29.14\",\n \"jasonmccreary/laravel-test-assertions\": \"^2.5\",\n \"larastan/larastan\": \"^3.1\",\n \"maglnet/composer-require-checker\": \"^4.8\",\n \"mockery/mockery\": \"^1.6\",\n \"nunomaduro/collision\": \"^8.6\",\n \"phpstan/phpstan\": \"^2.1\",\n \"phpunit/phpunit\": \"^11.5.50\",\n \"symfony/phpunit-bridge\": \"^7.0\",\n \"vimeo/psalm\": \"^6.5.0\"\n },\n \"autoload\": {\n \"classmap\": [\n \"database\"\n ],\n \"psr-4\": {\n \"Jiminny\\\\\": \"app/\",\n \"Tests\\\\\": \"tests/\",\n \"Database\\\\Factories\\\\\": \"database/factories/\",\n \"Database\\\\Seeders\\\\\": \"database/seeders/\",\n \"Microsoft\\\\Graph\\\\Generated\\\\Models\\\\\": \"app/Services/MeetingGenerator/Overrides/Microsoft/Graph/Generated/Models/\"\n },\n \"files\": [\n \"app/helpers.php\"\n ]\n },\n \"autoload-dev\": {\n \"classmap\": [\n \"tests/TestCase.php\"\n ],\n \"psr-4\": {\n \"Jiminny\\\\\": \"app/\",\n \"Tests\\\\\": \"tests/\"\n }\n },\n \"scripts\": {\n \"post-root-package-install\": [\n \"php -r \\\"file_exists('.env') || copy('.env.example', '.env');\\\"\"\n ],\n \"post-create-project-cmd\": [\n \"php artisan key:generate --ansi\"\n ],\n \"post-install-cmd\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postInstall\"\n ],\n \"post-update-cmd\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postUpdate\",\n \"php artisan ide-helper:generate\",\n \"php artisan ide-helper:meta\",\n \"@php artisan vendor:publish --tag=laravel-assets --ansi --force\"\n ],\n \"post-autoload-dump\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postAutoloadDump\",\n \"@php artisan package:discover --ansi\"\n ]\n },\n \"config\": {\n \"preferred-install\": \"dist\",\n \"sort-packages\": true,\n \"optimize-autoloader\": true,\n \"allow-plugins\": {\n \"infection/extension-installer\": true,\n \"php-http/discovery\": true,\n \"tbachert/spi\": true\n }\n },\n \"extra\": {\n \"laravel\": {\n \"dont-discover\": [\n \"laravel/dusk\"\n ]\n },\n \"metasyntactical/composer-plugin-license-check\": {\n \"whitelist\": [],\n \"blacklist\": [\n \"AGPL\"\n ]\n }\n },\n \"repositories\": [\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/PHP-FFMpeg/BinaryDriver.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-salesloft.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-aircall.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-pipedrive.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-ringcentral\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-dialpad.git\"\n }\n ],\n \"prefer-stable\": true\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Install","depth":3,"bounds":{"left":0.90957445,"top":0.07821229,"width":0.013297873,"height":0.013567438},"help_text":"Installs packages from composer.json, taking account of composer.lock","role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Update","depth":3,"bounds":{"left":0.9281915,"top":0.07821229,"width":0.016289894,"height":0.013567438},"help_text":"Installs latest appropriate versions of packages from composer.json","role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Show log","depth":3,"bounds":{"left":0.94980055,"top":0.07821229,"width":0.020279255,"height":0.013567438},"help_text":"Show log of Composer-related actions","role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8208934907902466404
|
8387909973189745310
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
18
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Providers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
use Jiminny\Component\DealRisks\DealRisk;
use Jiminny\Exceptions\LogicException;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Controllers\CustomerApi\CustomerApiController;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Ai\AiScorecard;
use Jiminny\Models\Ai\AiScorecardRule;
use Jiminny\Models\Ai\CrmTemplate;
use Jiminny\Models\Ai\CrmTemplateField;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\Group;
use Jiminny\Models\Invitation;
use Jiminny\Models\JobTitle;
use Jiminny\Models\Nudge;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Models\Session;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
class RouteServiceProvider extends ServiceProvider
{
/**
* Define your route model bindings, pattern filters, etc.
*/
public function boot(): void
{
$this->configureRateLimiting();
$this->routes(function (Router $router) {
$this
->mapHealthRoutes($router)
->mapWebhookRoutes($router)
->mapApiRoutes($router)
->mapApiV2Routes($router)
->mapWebRoutes($router)
->mapScimRoutes($router)
->mapCustomerApiRoutes($router)
->mapEmbeddedRoutes($router)
;
});
$this->defineRouteBindings();
}
private function mapHealthRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
], static function (Router $router) {
require base_path('routes/health.php');
});
return $this;
}
/**
* Define the route model bindings.
*/
protected function defineRouteBindings()
{
Route::pattern('teamSlug', '^[.\-_a-z0-9]*$');
Route::pattern('userSlug', '^[.\-_a-z0-9]*$');
Route::bind('participant', function ($value) {
return Participant::uuid($value);
});
Route::bind('participantA', static fn (string $value): Participant => Participant::uuid($value));
Route::bind('participantB', static fn (string $value): Participant => Participant::uuid($value));
Route::bind('connection', function ($value) {
return Connection::uuid($value);
});
Route::bind('activity', function ($value) {
return Activity::uuid($value);
});
Route::bind('session', function (string $value) {
return Session::uuid($value);
});
Route::bind('transcription', function ($value) {
return Activity\Transcription::uuid($value);
});
Route::bind('transcriptionProvider', static function (string $scorecardUuid): Models\TranscriptionProvider {
return Models\TranscriptionProvider::where(
'uuid',
Models\TranscriptionProvider::toOptimized($scorecardUuid)
)->firstOrFail();
});
Route::bind('coachingFeedback', function ($value) {
return CoachingFeedback::uuid($value);
});
Route::bind('team', function ($value) {
return Team::uuid($value);
});
Route::bind('crmTemplate', fn ($value) => CrmTemplate::uuid($value));
Route::bind('crmTemplateField', fn ($value) => CrmTemplateField::uuid($value));
Route::bind('aiScorecard', fn ($value) => AiScorecard::uuid($value));
Route::bind('aiScorecardRule', fn ($value) => AiScorecardRule::uuid($value));
Route::bind('group', function ($value) {
return Group::uuid($value);
});
Route::bind('user', function ($value) {
return User::uuid($value);
});
Route::bind('comment', function (string $value, \Illuminate\Routing\Route $route) {
$targetController = $route->getController();
if (! $targetController instanceof CommentContextInterface) {
throw new LogicException(
'Type hinting comment will require additional effort due to polymorphism.'
. ' Either implement CommentContextInterface or inject $commentId and process manually',
);
}
$commentClass = $targetController::getCommentImplementation();
return $commentClass::uuid($value);
});
Route::bind('track', function ($value) {
return Track::uuid($value);
});
Route::bind('invitation', function ($value) {
return Invitation::uuid($value);
});
Route::bind('playbook', function ($value) {
return Playbook::uuid($value);
});
Route::bind('category', function ($value) {
return PlaybookCategory::uuid($value);
});
Route::bind('coachingSection', function ($value) {
return CoachingSection::uuid($value);
});
Route::bind('coachingSectionCriterion', function ($value) {
return Models\CoachingSectionCriterion::uuid($value);
});
Route::bind('playlist', function ($value) {
return Playlist::uuid($value);
});
Route::bind('playlistActivity', static fn (string $value) => PlaylistActivity::uuid($value));
Route::bind('playlistTrack', static fn (string $value) => PlaylistActivity::uuid($value));
Route::bind('playlistShare', static fn (string $uuid) => Playlist\Share::uuid($uuid));
Route::bind('job', function ($value) {
return JobTitle::uuid($value);
});
Route::bind('notification', function ($value) {
return Activity\AvailabilityNotification::uuid($value);
});
Route::bind('activityMoment', function ($value) {
return Activity\Moment::uuid($value);
});
Route::bind('search', function ($value) {
return Activity\Search::uuid($value);
});
Route::bind('nudge', static function ($value): Nudge {
return Nudge::uuid($value);
});
Route::bind('theme', static function ($value): Models\PlaybackTheme {
return Models\PlaybackTheme::uuid($value);
});
Route::bind('topic', static function ($value): Models\PlaybackTheme\Topic {
return Models\PlaybackTheme\Topic::uuid($value);
});
Route::bind('topicTrigger', static function ($value): Models\PlaybackTheme\TopicTrigger {
return Models\PlaybackTheme\TopicTrigger::uuid($value);
});
Route::bind('vocabulary', static function (string $id): Models\Vocabulary {
return Models\Vocabulary::uuid($id);
});
Route::bind('teamDomain', function ($value) {
return Models\TeamDomain::uuid($value);
});
Route::bind('opportunity', function ($value) {
return Opportunity::uuid($value);
});
Route::bind('dealRisk', function ($value) {
return DealRisk::uuid($value);
});
Route::bind('layout', static fn (string $uuid) => Models\Crm\Layout::uuid($uuid));
Route::bind('scorecard', static function (string $scorecardUuid): Models\Scorecard\Scorecard {
return Models\Scorecard\Scorecard::where(
'uuid',
Models\Scorecard\Scorecard::toOptimized($scorecardUuid)
)->firstOrFail();
});
Route::bind('scorecardRule', static function (string $scorecardRuleUuid): Models\Scorecard\ScorecardRule {
return Models\Scorecard\ScorecardRule::where(
'uuid',
Models\Scorecard\ScorecardRule::toOptimized($scorecardRuleUuid)
)->firstOrFail();
});
Route::bind('askAnythingPrompt', function ($value) {
return Models\AskAnything\AskAnythingPrompt::uuid($value);
});
}
/**
* Define the routes for the application.
*
* @param \Illuminate\Routing\Router $router
*/
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*/
private function mapWebRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['web'],
], static function (Router $router) {
require base_path('routes/web.php');
});
return $this;
}
/**
* Define the "Embedded" routes for the application.
*
* These routes depend on a Partitioned cookie,
* and are accessible only after login.
*/
private function mapEmbeddedRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['embedded'],
'prefix' => 'embedded',
], static function (Router $router) {
require base_path('routes/embedded.php');
});
return $this;
}
private function mapWebhookRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['webhook'],
'prefix' => 'webhook',
], function (Router $router) {
require base_path('routes/webhook.php');
});
return $this;
}
/**
* Define the "api" routes for the application.
*/
private function mapApiRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['api'],
'prefix' => 'api/v1',
], function (Router $router) {
require base_path('routes/api.php');
});
return $this;
}
private function mapApiV2Routes(Router $router): self
{
$router->group([
'middleware' => ['api', 'auth:api'],
'prefix' => 'api/v2',
], static function (Router $router) {
require base_path('routes/api_v2.php');
});
return $this;
}
private function mapScimRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['scim'],
'prefix' => 'scim/v2',
], function (Router $router) {
require base_path('routes/scim.php');
});
return $this;
}
private function mapCustomerApiRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['customer-api'],
'prefix' => 'customer/api/v1',
], function (Router $router) {
require base_path('routes/customer_api.php');
});
return $this;
}
/** Configure the rate limiters for the application. */
protected function configureRateLimiting(): void
{
RateLimiter::for('api', static function (Request $request) {
$user = $request->user();
if ($user instanceof User || $user instanceof Models\Partner) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
if (! is_string($subject)) {
throw new LogicException('Unable to identify subject to rate limit');
}
return Limit::perMinute(120)->by($subject);
});
RateLimiter::for('customer-api', static function (Request $request) {
// Get the token using the same method as the middleware
$token = null;
if ($request->bearerToken() !== null) {
$token = $request->bearerToken();
} elseif ($request->get('api_token') !== null) {
$token = $request->get('api_token');
} elseif ($request->post('api_token') !== null) {
$token = $request->post('api_token');
}
// If we have a valid token, get the team ID for rate limiting
if ($token !== null && strlen($token) === Team::CUSTOMER_API_TOKEN_LENGTH) {
$tokens = Cache::remember(Team::CUSTOMER_API_TOKENS_CACHE, 60 * 60, static function () {
return Team::query()->whereNotNull('api_token')->pluck('id', 'api_token');
});
$hashedToken = hash('sha256', $token);
if (is_countable($tokens) && ! empty($tokens) && isset($tokens[$hashedToken])) {
$teamId = $tokens[$hashedToken];
$team = Team::find($teamId);
if ($team) {
$subject = $team->getUuid();
// Special rate limit for Funding Circle and Cision.
if (in_array($subject, [
'9762611a-fc86-4234-baba-a436de26e774', '1151b5b0-0680-4190-b30c-72649280837d',
'16b50903-a115-448f-877c-6c01e8b45f5d', 'bfc1c304-83b2-47a3-a7ce-1d28b0e1e90e',
'447ee473-7dd8-41bc-a702-9322e5b0ec5b'])) {
// Route-specific rate limiting
if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {
return Limit::perMinute(40)->by($subject);
}
return Limit::perMinute(1000)->by($subject);
}
// Route-specific rate limiting
if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {
return Limit::perMinute(30)->by($subject);
}
return Limit::perMinute(120)->by($subject);
}
}
}
// Default fallback to IP-based limiting
return Limit::perMinute(120)->by($request->ip());
});
RateLimiter::for('conference-consent', static function (Request $request) {
// Rate limit by IP address for public consent endpoint
// Allow 10 requests per minute per IP to prevent abuse while allowing legitimate use
return Limit::perMinute(10)->by($request->ip());
});
RateLimiter::for('activity-export-shareable-link', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(30)->by($subject);
});
RateLimiter::for('activity-export', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(10)->by($subject);
});
RateLimiter::for('transcription-download', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(3)->by($subject);
});
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
14
Previous Highlighted Error
Next Highlighted Error
{
"name": "jiminny/app",
"description": "The Jiminny Platform.",
"keywords": [
"training",
"salesforce",
"conference"
],
"license": "MIT",
"type": "project",
"require": {
"php": "^8.3",
"ext-ctype": "*",
"ext-curl": "*",
"ext-date": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-filter": "*",
"ext-gd": "*",
"ext-gmp": "*",
"ext-hash": "*",
"ext-iconv": "*",
"ext-igbinary": "*",
"ext-imagick": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mailparse": "*",
"ext-mbstring": "*",
"ext-mysqlnd": "*",
"ext-openssl": "*",
"ext-pcntl": "*",
"ext-pcre": "*",
"ext-pdo": "*",
"ext-pdo_mysql": "*",
"ext-phar": "*",
"ext-phpiredis": "*",
"ext-posix": "*",
"ext-readline": "*",
"ext-redis": "*",
"ext-reflection": "*",
"ext-session": "*",
"ext-simplexml": "*",
"ext-sockets": "*",
"ext-spl": "*",
"ext-tokenizer": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zend-opcache": "*",
"ext-zip": "*",
"ext-zlib": "*",
"lib-curl": "*",
"lib-curl-openssl": "*",
"lib-curl-zlib": "*",
"lib-date-timelib": "*",
"lib-date-zoneinfo": "*",
"lib-fileinfo-libmagic": "*",
"lib-gd": "*",
"lib-gd-freetype": "*",
"lib-gd-libjpeg": "*",
"lib-gd-libpng": "*",
"lib-gmp": "*",
"lib-icu": "*",
"lib-icu-cldr": "*",
"lib-icu-unicode": "*",
"lib-imagick-imagemagick": "*",
"lib-libxml": "*",
"lib-mbstring-libmbfl": "*",
"lib-mbstring-oniguruma": "*",
"lib-openssl": "*",
"lib-pcre": "*",
"lib-pcre-unicode": "*",
"lib-zip-libzip": "*",
"lib-zlib": "*",
"24slides/laravel-saml2": "^2.4",
"adam-paterson/oauth2-slack": "^1.1",
"asimlqt/php-google-spreadsheet-client": "^3.0",
"aws/aws-sdk-php": "^3.368",
"aws/aws-sdk-php-laravel": "^3.10",
"bepsvpt/secure-headers": "^9.0",
"chadhutchins/oauth2-slack": "^1.2",
"chaseconey/laravel-datadog-helper": "^1.2",
"chrisyue/php-m3u8": "4.0.3",
"daniti/oauth2-pipedrive": "dev-master",
"devio/pipedrive": "^2.6",
"doctrine/dbal": "^4.0",
"elasticsearch/elasticsearch": "^7.11",
"erusev/parsedown": "^1.7",
"fakerphp/faker": "^1.23",
"firebase/php-jwt": "^7.0",
"flipboxdigital/oauth2-hubspot": "1.0.1",
"giggsey/libphonenumber-for-php": "^8.12",
"google/apiclient": "^2.19",
"google/apiclient-services": "~0.360",
"google/apps-meet": "^0.5.1",
"guzzlehttp/guzzle": "^7.8",
"guzzlehttp/psr7": "^2.6",
"halaxa/json-machine": "^1.2",
"html2text/html2text": "^4.3",
"hubspot/api-client": "~5.0.0",
"hubspot/hubspot-php": "^5.2.0",
"intercom/intercom-php": "^4.5",
"intervention/image": "^3.4",
"jakeasmith/http_build_url": "^1.0",
"jdavidbakr/cloudfront-proxies": "^1.7",
"jeremykendall/php-domain-parser": "^6.3",
"jiminny/oauth2-aircall": "dev-master",
"jiminny/oauth2-bullhorn": "^0.2.0",
"jiminny/oauth2-dialpad": "dev-master",
"jiminny/oauth2-salesloft": "dev-master",
"jolicode/slack-php-api": "^4.5.0",
"kalnoy/nestedset": "*",
"laravel/framework": "^12.28",
"laravel/helpers": "^1.7",
"laravel/passport": "^13.0",
"laravel/slack-notification-channel": "^3.4",
"laravel/tinker": "^2.10.1",
"laravel/ui": "^4.6",
"laravolt/avatar": "^6.1",
"league/flysystem": "^3.0",
"league/flysystem-aws-s3-v3": "^3.0",
"league/fractal": "*",
"league/oauth2-client": "^2.7",
"league/oauth2-google": "^4.0",
"league/oauth2-linkedin": "^5.1",
"league/oauth2-server": "^9.2",
"league/statsd": "^2.0",
"markrogoyski/math-php": "^2.7.0",
"microsoft/microsoft-graph": "^2.51",
"monolog/monolog": "^3.0",
"nesbot/carbon": "^3.8",
"nette/caching": "*",
"phlib/sms-length": "^2.0",
"php-ffmpeg/php-ffmpeg": "^1.2",
"php-http/client-common": "^2.7",
"php-http/curl-client": "^2.3",
"php-http/httplug": "^2.2",
"php-http/message": "^1.16",
"phpseclib/phpseclib": "^3.0.36",
"propaganistas/laravel-phone": "^5.3",
"psr/cache": "^3.0",
"psr/http-message": "^2.0",
"psr/log": "^3.0",
"psr/simple-cache": "^3.0",
"pusher/pusher-php-server": "7.2.3",
"ramsey/uuid": "^4.2",
"ringcentral/ringcentral-php": "3.0.0",
"rmccue/requests": "^2.0",
"ruflin/elastica": "^7.1.1",
"santigarcor/laratrust": "^8.4",
"sentry/sentry": "4.13.0",
"sentry/sentry-laravel": "~4.13.0",
"shiftonelabs/laravel-sqs-fifo-queue": "^3.0",
"spatie/fractalistic": "^2.9",
"spatie/laravel-fractal": "^6.3",
"spatie/laravel-ignition": "^2.9",
"spatie/laravel-webhook-server": "^3.8",
"staudenmeir/belongs-to-through": "^2.17",
"stevenmaguire/oauth2-salesforce": "^2.0",
"symfony/cache": "^7.2",
"symfony/console": "^7.2",
"symfony/css-selector": "^7.2",
"symfony/debug": "^4.4",
"symfony/dom-crawler": "^7.2",
"symfony/expression-language": "^7.2",
"symfony/finder": "^7.2",
"symfony/http-client": "^7.3",
"symfony/http-foundation": "^7.2",
"symfony/http-kernel": "^7.2",
"symfony/postmark-mailer": "^7.3",
"symfony/process": "^7.3",
"symfony/property-access": "^7.2",
"symfony/psr-http-message-bridge": "^7.0",
"symfony/var-dumper": "^7.2",
"symfony/workflow": "^7.2",
"tecnickcom/tcpdf": "^6.11",
"thenetworg/oauth2-azure": "dev-master",
"tmannherz/oauth2-ringcentral": "dev-master",
"twilio/sdk": "^8.3",
"vanderlee/php-sentence": "^1.0",
"vinkla/hashids": "^13.0",
"vlucas/phpdotenv": "^5.4",
"wildbit/postmark-php": "^6.0",
"willdurand/email-reply-parser": "^2.8",
"zbateson/mail-mime-parser": "^3.0.4"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.15",
"barryvdh/laravel-ide-helper": "^3.5",
"brianium/paratest": "^7.5",
"browserstack/browserstack-local": "^1.1.0",
"filp/whoops": "^2.9",
"friendsofphp/php-cs-fixer": "^3.66",
"infection/infection": "^0.29.14",
"jasonmccreary/laravel-test-assertions": "^2.5",
"larastan/larastan": "^3.1",
"maglnet/composer-require-checker": "^4.8",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^11.5.50",
"symfony/phpunit-bridge": "^7.0",
"vimeo/psalm": "^6.5.0"
},
"autoload": {
"classmap": [
"database"
],
"psr-4": {
"Jiminny\\": "app/",
"Tests\\": "tests/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/",
"Microsoft\\Graph\\Generated\\Models\\": "app/Services/MeetingGenerator/Overrides/Microsoft/Graph/Generated/Models/"
},
"files": [
"app/helpers.php"
]
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
],
"psr-4": {
"Jiminny\\": "app/",
"Tests\\": "tests/"
}
},
"scripts": {
"post-root-package-install": [
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"php artisan key:generate --ansi"
],
"post-install-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postInstall"
],
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan ide-helper:generate",
"php artisan ide-helper:meta",
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
]
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true,
"allow-plugins": {
"infection/extension-installer": true,
"php-http/discovery": true,
"tbachert/spi": true
}
},
"extra": {
"laravel": {
"dont-discover": [
"laravel/dusk"
]
},
"metasyntactical/composer-plugin-license-check": {
"whitelist": [],
"blacklist": [
"AGPL"
]
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/PHP-FFMpeg/BinaryDriver.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-salesloft.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-aircall.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-pipedrive.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-ringcentral"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-dialpad.git"
}
],
"prefer-stable": true
}
Install
Update
Show log
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
53773
|
1163
|
30
|
2026-04-20T08:27:43.575232+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-20/1776 /Users/lukas/.screenpipe/data/data/2026-04-20/1776673663575_m2.jpg...
|
PhpStorm
|
faVsco.js – RouteServiceProvider.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Providers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
use Jiminny\Component\DealRisks\DealRisk;
use Jiminny\Exceptions\LogicException;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Controllers\CustomerApi\CustomerApiController;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Ai\AiScorecard;
use Jiminny\Models\Ai\AiScorecardRule;
use Jiminny\Models\Ai\CrmTemplate;
use Jiminny\Models\Ai\CrmTemplateField;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\Group;
use Jiminny\Models\Invitation;
use Jiminny\Models\JobTitle;
use Jiminny\Models\Nudge;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Models\Session;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
class RouteServiceProvider extends ServiceProvider
{
/**
* Define your route model bindings, pattern filters, etc.
*/
public function boot(): void
{
$this->configureRateLimiting();
$this->routes(function (Router $router) {
$this
->mapHealthRoutes($router)
->mapWebhookRoutes($router)
->mapApiRoutes($router)
->mapApiV2Routes($router)
->mapWebRoutes($router)
->mapScimRoutes($router)
->mapCustomerApiRoutes($router)
->mapEmbeddedRoutes($router)
;
});
$this->defineRouteBindings();
}
private function mapHealthRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
], static function (Router $router) {
require base_path('routes/health.php');
});
return $this;
}
/**
* Define the route model bindings.
*/
protected function defineRouteBindings()
{
Route::pattern('teamSlug', '^[.\-_a-z0-9]*$');
Route::pattern('userSlug', '^[.\-_a-z0-9]*$');
Route::bind('participant', function ($value) {
return Participant::uuid($value);
});
Route::bind('participantA', static fn (string $value): Participant => Participant::uuid($value));
Route::bind('participantB', static fn (string $value): Participant => Participant::uuid($value));
Route::bind('connection', function ($value) {
return Connection::uuid($value);
});
Route::bind('activity', function ($value) {
return Activity::uuid($value);
});
Route::bind('session', function (string $value) {
return Session::uuid($value);
});
Route::bind('transcription', function ($value) {
return Activity\Transcription::uuid($value);
});
Route::bind('transcriptionProvider', static function (string $scorecardUuid): Models\TranscriptionProvider {
return Models\TranscriptionProvider::where(
'uuid',
Models\TranscriptionProvider::toOptimized($scorecardUuid)
)->firstOrFail();
});
Route::bind('coachingFeedback', function ($value) {
return CoachingFeedback::uuid($value);
});
Route::bind('team', function ($value) {
return Team::uuid($value);
});
Route::bind('crmTemplate', fn ($value) => CrmTemplate::uuid($value));
Route::bind('crmTemplateField', fn ($value) => CrmTemplateField::uuid($value));
Route::bind('aiScorecard', fn ($value) => AiScorecard::uuid($value));
Route::bind('aiScorecardRule', fn ($value) => AiScorecardRule::uuid($value));
Route::bind('group', function ($value) {
return Group::uuid($value);
});
Route::bind('user', function ($value) {
return User::uuid($value);
});
Route::bind('comment', function (string $value, \Illuminate\Routing\Route $route) {
$targetController = $route->getController();
if (! $targetController instanceof CommentContextInterface) {
throw new LogicException(
'Type hinting comment will require additional effort due to polymorphism.'
. ' Either implement CommentContextInterface or inject $commentId and process manually',
);
}
$commentClass = $targetController::getCommentImplementation();
return $commentClass::uuid($value);
});
Route::bind('track', function ($value) {
return Track::uuid($value);
});
Route::bind('invitation', function ($value) {
return Invitation::uuid($value);
});
Route::bind('playbook', function ($value) {
return Playbook::uuid($value);
});
Route::bind('category', function ($value) {
return PlaybookCategory::uuid($value);
});
Route::bind('coachingSection', function ($value) {
return CoachingSection::uuid($value);
});
Route::bind('coachingSectionCriterion', function ($value) {
return Models\CoachingSectionCriterion::uuid($value);
});
Route::bind('playlist', function ($value) {
return Playlist::uuid($value);
});
Route::bind('playlistActivity', static fn (string $value) => PlaylistActivity::uuid($value));
Route::bind('playlistTrack', static fn (string $value) => PlaylistActivity::uuid($value));
Route::bind('playlistShare', static fn (string $uuid) => Playlist\Share::uuid($uuid));
Route::bind('job', function ($value) {
return JobTitle::uuid($value);
});
Route::bind('notification', function ($value) {
return Activity\AvailabilityNotification::uuid($value);
});
Route::bind('activityMoment', function ($value) {
return Activity\Moment::uuid($value);
});
Route::bind('search', function ($value) {
return Activity\Search::uuid($value);
});
Route::bind('nudge', static function ($value): Nudge {
return Nudge::uuid($value);
});
Route::bind('theme', static function ($value): Models\PlaybackTheme {
return Models\PlaybackTheme::uuid($value);
});
Route::bind('topic', static function ($value): Models\PlaybackTheme\Topic {
return Models\PlaybackTheme\Topic::uuid($value);
});
Route::bind('topicTrigger', static function ($value): Models\PlaybackTheme\TopicTrigger {
return Models\PlaybackTheme\TopicTrigger::uuid($value);
});
Route::bind('vocabulary', static function (string $id): Models\Vocabulary {
return Models\Vocabulary::uuid($id);
});
Route::bind('teamDomain', function ($value) {
return Models\TeamDomain::uuid($value);
});
Route::bind('opportunity', function ($value) {
return Opportunity::uuid($value);
});
Route::bind('dealRisk', function ($value) {
return DealRisk::uuid($value);
});
Route::bind('layout', static fn (string $uuid) => Models\Crm\Layout::uuid($uuid));
Route::bind('scorecard', static function (string $scorecardUuid): Models\Scorecard\Scorecard {
return Models\Scorecard\Scorecard::where(
'uuid',
Models\Scorecard\Scorecard::toOptimized($scorecardUuid)
)->firstOrFail();
});
Route::bind('scorecardRule', static function (string $scorecardRuleUuid): Models\Scorecard\ScorecardRule {
return Models\Scorecard\ScorecardRule::where(
'uuid',
Models\Scorecard\ScorecardRule::toOptimized($scorecardRuleUuid)
)->firstOrFail();
});
Route::bind('askAnythingPrompt', function ($value) {
return Models\AskAnything\AskAnythingPrompt::uuid($value);
});
}
/**
* Define the routes for the application.
*
* @param \Illuminate\Routing\Router $router
*/
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*/
private function mapWebRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['web'],
], static function (Router $router) {
require base_path('routes/web.php');
});
return $this;
}
/**
* Define the "Embedded" routes for the application.
*
* These routes depend on a Partitioned cookie,
* and are accessible only after login.
*/
private function mapEmbeddedRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['embedded'],
'prefix' => 'embedded',
], static function (Router $router) {
require base_path('routes/embedded.php');
});
return $this;
}
private function mapWebhookRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['webhook'],
'prefix' => 'webhook',
], function (Router $router) {
require base_path('routes/webhook.php');
});
return $this;
}
/**
* Define the "api" routes for the application.
*/
private function mapApiRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['api'],
'prefix' => 'api/v1',
], function (Router $router) {
require base_path('routes/api.php');
});
return $this;
}
private function mapApiV2Routes(Router $router): self
{
$router->group([
'middleware' => ['api', 'auth:api'],
'prefix' => 'api/v2',
], static function (Router $router) {
require base_path('routes/api_v2.php');
});
return $this;
}
private function mapScimRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['scim'],
'prefix' => 'scim/v2',
], function (Router $router) {
require base_path('routes/scim.php');
});
return $this;
}
private function mapCustomerApiRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['customer-api'],
'prefix' => 'customer/api/v1',
], function (Router $router) {
require base_path('routes/customer_api.php');
});
return $this;
}
/** Configure the rate limiters for the application. */
protected function configureRateLimiting(): void
{
RateLimiter::for('api', static function (Request $request) {
$user = $request->user();
if ($user instanceof User || $user instanceof Models\Partner) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
if (! is_string($subject)) {
throw new LogicException('Unable to identify subject to rate limit');
}
return Limit::perMinute(120)->by($subject);
});
RateLimiter::for('customer-api', static function (Request $request) {
// Get the token using the same method as the middleware
$token = null;
if ($request->bearerToken() !== null) {
$token = $request->bearerToken();
} elseif ($request->get('api_token') !== null) {
$token = $request->get('api_token');
} elseif ($request->post('api_token') !== null) {
$token = $request->post('api_token');
}
// If we have a valid token, get the team ID for rate limiting
if ($token !== null && strlen($token) === Team::CUSTOMER_API_TOKEN_LENGTH) {
$tokens = Cache::remember(Team::CUSTOMER_API_TOKENS_CACHE, 60 * 60, static function () {
return Team::query()->whereNotNull('api_token')->pluck('id', 'api_token');
});
$hashedToken = hash('sha256', $token);
if (is_countable($tokens) && ! empty($tokens) && isset($tokens[$hashedToken])) {
$teamId = $tokens[$hashedToken];
$team = Team::find($teamId);
if ($team) {
$subject = $team->getUuid();
// Special rate limit for Funding Circle and Cision.
if (in_array($subject, [
'9762611a-fc86-4234-baba-a436de26e774', '1151b5b0-0680-4190-b30c-72649280837d',
'16b50903-a115-448f-877c-6c01e8b45f5d', 'bfc1c304-83b2-47a3-a7ce-1d28b0e1e90e',
'447ee473-7dd8-41bc-a702-9322e5b0ec5b'])) {
// Route-specific rate limiting
if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {
return Limit::perMinute(40)->by($subject);
}
return Limit::perMinute(1000)->by($subject);
}
// Route-specific rate limiting
if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {
return Limit::perMinute(30)->by($subject);
}
return Limit::perMinute(120)->by($subject);
}
}
}
// Default fallback to IP-based limiting
return Limit::perMinute(120)->by($request->ip());
});
RateLimiter::for('conference-consent', static function (Request $request) {
// Rate limit by IP address for public consent endpoint
// Allow 10 requests per minute per IP to prevent abuse while allowing legitimate use
return Limit::perMinute(10)->by($request->ip());
});
RateLimiter::for('activity-export-shareable-link', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(30)->by($subject);
});
RateLimiter::for('activity-export', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(10)->by($subject);
});
RateLimiter::for('transcription-download', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(3)->by($subject);
});
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
14
Previous Highlighted Error
Next Highlighted Error
{
"name": "jiminny/app",
"description": "The Jiminny Platform.",
"keywords": [
"training",
"salesforce",
"conference"
],
"license": "MIT",
"type": "project",
"require": {
"php": "^8.3",
"ext-ctype": "*",
"ext-curl": "*",
"ext-date": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-filter": "*",
"ext-gd": "*",
"ext-gmp": "*",
"ext-hash": "*",
"ext-iconv": "*",
"ext-igbinary": "*",
"ext-imagick": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mailparse": "*",
"ext-mbstring": "*",
"ext-mysqlnd": "*",
"ext-openssl": "*",
"ext-pcntl": "*",
"ext-pcre": "*",
"ext-pdo": "*",
"ext-pdo_mysql": "*",
"ext-phar": "*",
"ext-phpiredis": "*",
"ext-posix": "*",
"ext-readline": "*",
"ext-redis": "*",
"ext-reflection": "*",
"ext-session": "*",
"ext-simplexml": "*",
"ext-sockets": "*",
"ext-spl": "*",
"ext-tokenizer": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zend-opcache": "*",
"ext-zip": "*",
"ext-zlib": "*",
"lib-curl": "*",
"lib-curl-openssl": "*",
"lib-curl-zlib": "*",
"lib-date-timelib": "*",
"lib-date-zoneinfo": "*",
"lib-fileinfo-libmagic": "*",
"lib-gd": "*",
"lib-gd-freetype": "*",
"lib-gd-libjpeg": "*",
"lib-gd-libpng": "*",
"lib-gmp": "*",
"lib-icu": "*",
"lib-icu-cldr": "*",
"lib-icu-unicode": "*",
"lib-imagick-imagemagick": "*",
"lib-libxml": "*",
"lib-mbstring-libmbfl": "*",
"lib-mbstring-oniguruma": "*",
"lib-openssl": "*",
"lib-pcre": "*",
"lib-pcre-unicode": "*",
"lib-zip-libzip": "*",
"lib-zlib": "*",
"24slides/laravel-saml2": "^2.4",
"adam-paterson/oauth2-slack": "^1.1",
"asimlqt/php-google-spreadsheet-client": "^3.0",
"aws/aws-sdk-php": "^3.368",
"aws/aws-sdk-php-laravel": "^3.10",
"bepsvpt/secure-headers": "^9.0",
"chadhutchins/oauth2-slack": "^1.2",
"chaseconey/laravel-datadog-helper": "^1.2",
"chrisyue/php-m3u8": "4.0.3",
"daniti/oauth2-pipedrive": "dev-master",
"devio/pipedrive": "^2.6",
"doctrine/dbal": "^4.0",
"elasticsearch/elasticsearch": "^7.11",
"erusev/parsedown": "^1.7",
"fakerphp/faker": "^1.23",
"firebase/php-jwt": "^7.0",
"flipboxdigital/oauth2-hubspot": "1.0.1",
"giggsey/libphonenumber-for-php": "^8.12",
"google/apiclient": "^2.19",
"google/apiclient-services": "~0.360",
"google/apps-meet": "^0.5.1",
"guzzlehttp/guzzle": "^7.8",
"guzzlehttp/psr7": "^2.6",
"halaxa/json-machine": "^1.2",
"html2text/html2text": "^4.3",
"hubspot/api-client": "~5.0.0",
"hubspot/hubspot-php": "^5.2.0",
"intercom/intercom-php": "^4.5",
"intervention/image": "^3.4",
"jakeasmith/http_build_url": "^1.0",
"jdavidbakr/cloudfront-proxies": "^1.7",
"jeremykendall/php-domain-parser": "^6.3",
"jiminny/oauth2-aircall": "dev-master",
"jiminny/oauth2-bullhorn": "^0.2.0",
"jiminny/oauth2-dialpad": "dev-master",
"jiminny/oauth2-salesloft": "dev-master",
"jolicode/slack-php-api": "^4.5.0",
"kalnoy/nestedset": "*",
"laravel/framework": "^12.28",
"laravel/helpers": "^1.7",
"laravel/passport": "^13.0",
"laravel/slack-notification-channel": "^3.4",
"laravel/tinker": "^2.10.1",
"laravel/ui": "^4.6",
"laravolt/avatar": "^6.1",
"league/flysystem": "^3.0",
"league/flysystem-aws-s3-v3": "^3.0",
"league/fractal": "*",
"league/oauth2-client": "^2.7",
"league/oauth2-google": "^4.0",
"league/oauth2-linkedin": "^5.1",
"league/oauth2-server": "^9.2",
"league/statsd": "^2.0",
"markrogoyski/math-php": "^2.7.0",
"microsoft/microsoft-graph": "^2.51",
"monolog/monolog": "^3.0",
"nesbot/carbon": "^3.8",
"nette/caching": "*",
"phlib/sms-length": "^2.0",
"php-ffmpeg/php-ffmpeg": "^1.2",
"php-http/client-common": "^2.7",
"php-http/curl-client": "^2.3",
"php-http/httplug": "^2.2",
"php-http/message": "^1.16",
"phpseclib/phpseclib": "^3.0.36",
"propaganistas/laravel-phone": "^5.3",
"psr/cache": "^3.0",
"psr/http-message": "^2.0",
"psr/log": "^3.0",
"psr/simple-cache": "^3.0",
"pusher/pusher-php-server": "7.2.3",
"ramsey/uuid": "^4.2",
"ringcentral/ringcentral-php": "3.0.0",
"rmccue/requests": "^2.0",
"ruflin/elastica": "^7.1.1",
"santigarcor/laratrust": "^8.4",
"sentry/sentry": "4.13.0",
"sentry/sentry-laravel": "~4.13.0",
"shiftonelabs/laravel-sqs-fifo-queue": "^3.0",
"spatie/fractalistic": "^2.9",
"spatie/laravel-fractal": "^6.3",
"spatie/laravel-ignition": "^2.9",
"spatie/laravel-webhook-server": "^3.8",
"staudenmeir/belongs-to-through": "^2.17",
"stevenmaguire/oauth2-salesforce": "^2.0",
"symfony/cache": "^7.2",
"symfony/console": "^7.2",
"symfony/css-selector": "^7.2",
"symfony/debug": "^4.4",
"symfony/dom-crawler": "^7.2",
"symfony/expression-language": "^7.2",
"symfony/finder": "^7.2",
"symfony/http-client": "^7.3",
"symfony/http-foundation": "^7.2",
"symfony/http-kernel": "^7.2",
"symfony/postmark-mailer": "^7.3",
"symfony/process": "^7.3",
"symfony/property-access": "^7.2",
"symfony/psr-http-message-bridge": "^7.0",
"symfony/var-dumper": "^7.2",
"symfony/workflow": "^7.2",
"tecnickcom/tcpdf": "^6.11",
"thenetworg/oauth2-azure": "dev-master",
"tmannherz/oauth2-ringcentral": "dev-master",
"twilio/sdk": "^8.3",
"vanderlee/php-sentence": "^1.0",
"vinkla/hashids": "^13.0",
"vlucas/phpdotenv": "^5.4",
"wildbit/postmark-php": "^6.0",
"willdurand/email-reply-parser": "^2.8",
"zbateson/mail-mime-parser": "^3.0.4"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.15",
"barryvdh/laravel-ide-helper": "^3.5",
"brianium/paratest": "^7.5",
"browserstack/browserstack-local": "^1.1.0",
"filp/whoops": "^2.9",
"friendsofphp/php-cs-fixer": "^3.66",
"infection/infection": "^0.29.14",
"jasonmccreary/laravel-test-assertions": "^2.5",
"larastan/larastan": "^3.1",
"maglnet/composer-require-checker": "^4.8",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^11.5.50",
"symfony/phpunit-bridge": "^7.0",
"vimeo/psalm": "^6.5.0"
},
"autoload": {
"classmap": [
"database"
],
"psr-4": {
"Jiminny\\": "app/",
"Tests\\": "tests/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/",
"Microsoft\\Graph\\Generated\\Models\\": "app/Services/MeetingGenerator/Overrides/Microsoft/Graph/Generated/Models/"
},
"files": [
"app/helpers.php"
]
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
],
"psr-4": {
"Jiminny\\": "app/",
"Tests\\": "tests/"
}
},
"scripts": {
"post-root-package-install": [
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"php artisan key:generate --ansi"
],
"post-install-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postInstall"
],
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan ide-helper:generate",
"php artisan ide-helper:meta",
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
]
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true,
"allow-plugins": {
"infection/extension-installer": true,
"php-http/discovery": true,
"tbachert/spi": true
}
},
"extra": {
"laravel": {
"dont-discover": [
"laravel/dusk"
]
},
"metasyntactical/composer-plugin-license-check": {
"whitelist": [],
"blacklist": [
"AGPL"
]
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/PHP-FFMpeg/BinaryDriver.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-salesloft.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-aircall.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-pipedrive.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-ringcentral"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-dialpad.git"
}
],
"prefer-stable": true
}
Install
Update
Show log
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Configuration, folder
Console, folder
Commands, folder
Activities, folder
Analytics, folder
Calendars, folder
Crm, folder
DealInsights, folder
Dev, folder
Dialers, folder
DTOs, folder
Elasticsearch, folder
EngagementStats, folder
GeckoExport, folder
Livestream, folder
Mailboxes, folder
Migrate, folder
PlaybackThemes, folder
Playbooks, folder
Playlists, folder
Postmark, folder
ProphetAi, folder
Reports, folder
AutomatedReportsCommand.php, class
AutomatedReportsRetentionPolicyCommand.php, class
AutomatedReportsSendCommand.php, class
CreateMockAskJiminnyReportResultCommand.php, class
DeleteReportCommand.php, class
GenerateMarketingReport.php, class
Team.php, class
Usage.php, class
Slack, folder
Teams, folder
Tracks, folder
Transcription, folder
Twilio, folder
Users, folder
Vocabulary, folder
Zoom, folder
CoachingFeedbacksUpdateEsActivities.php, class
Command.php, class
CreateDatabaseUsers.php, class
DatabaseTableCount.php, class
DeleteOldAiCrmNotesCommand.php, class
DeleteS3LeftoversCommand.php, class
DevPostmanCommand.php, final class
DiarizeViaAiParticipantIdentificationCommand.php, class
EncryptTokensCommand.php, class
EngagementStatsRegenerateCommand.php, class
FeatureFlagsHelper.php
FixCrossTenantIssues.php, class
FlushRolesPermissionsCache.php, class
GenerateInternalWebhookToken.php, class
GroupSetDefaultLanguageCommand.php, final class
HelperTruncateCoachingTables.php, class
HubspotJournalPollingCommand.php, class
HubspotWebhookServiceCommand.php, class
ImportRecording.php, class
ImportUsersFromCsvFile.php, final class
IterateUsersCommand.php, abstract class
JiminnyCacheClearCommand.php, class
JiminnyDebugCommand.php, class
JiminnySetEncryptedTokenManagerModeCommand.php, class
JiminnyTokenInfoCommand.php, class
MakeSlackLiveCoachingChatNotesOn.php, class
ManageScimForTeam.php, class
MarkBranchForEnvironmentPipelineCommand.php, class
MuteOrganizerChannel.php, class
PhpApm.php, class
PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class
PurgeConferences.php, class
PurgeSoftDeletedOpportunitiesCommand.php, class
PurgeSyncBatchesCommand.php, class
RecalculateDealRisksCommand.php, class
RemoveDeleteMarkersCommand.php, class
RemoveExpiredNudgesCommand.php, class
RemoveUnusedParticipantSpeechesCommand.php, class
ResetElasticSearch.php, class
RestoreActivityCrmProviderIdCommand.php, class
RestoreActivityTypeCommand.php, class
SeedActivities.php, class
SyncActivity.php, class
TrackImported.php, class...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.796875,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TrackAutomatedReportGeneratedEventTest","depth":6,"bounds":{"left":0.8121675,"top":0.019952115,"width":0.103390954,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TrackAutomatedReportGeneratedEventTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Providers;\n\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\RateLimiter;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Routing\\Router;\nuse Illuminate\\Foundation\\Support\\Providers\\RouteServiceProvider as ServiceProvider;\nuse Illuminate\\Support\\Facades\\Route;\nuse Jiminny\\Component\\DealRisks\\DealRisk;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Controllers\\CustomerApi\\CustomerApiController;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Ai\\AiScorecard;\nuse Jiminny\\Models\\Ai\\AiScorecardRule;\nuse Jiminny\\Models\\Ai\\CrmTemplate;\nuse Jiminny\\Models\\Ai\\CrmTemplateField;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Invitation;\nuse Jiminny\\Models\\JobTitle;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Models\\Session;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\n\nclass RouteServiceProvider extends ServiceProvider\n{\n /**\n * Define your route model bindings, pattern filters, etc.\n */\n public function boot(): void\n {\n $this->configureRateLimiting();\n\n $this->routes(function (Router $router) {\n $this\n ->mapHealthRoutes($router)\n ->mapWebhookRoutes($router)\n ->mapApiRoutes($router)\n ->mapApiV2Routes($router)\n ->mapWebRoutes($router)\n ->mapScimRoutes($router)\n ->mapCustomerApiRoutes($router)\n ->mapEmbeddedRoutes($router)\n ;\n });\n $this->defineRouteBindings();\n }\n\n private function mapHealthRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n ], static function (Router $router) {\n require base_path('routes/health.php');\n });\n\n return $this;\n }\n\n /**\n * Define the route model bindings.\n */\n protected function defineRouteBindings()\n {\n Route::pattern('teamSlug', '^[.\\-_a-z0-9]*$');\n Route::pattern('userSlug', '^[.\\-_a-z0-9]*$');\n\n Route::bind('participant', function ($value) {\n return Participant::uuid($value);\n });\n\n Route::bind('participantA', static fn (string $value): Participant => Participant::uuid($value));\n\n Route::bind('participantB', static fn (string $value): Participant => Participant::uuid($value));\n\n Route::bind('connection', function ($value) {\n return Connection::uuid($value);\n });\n\n Route::bind('activity', function ($value) {\n return Activity::uuid($value);\n });\n\n Route::bind('session', function (string $value) {\n return Session::uuid($value);\n });\n\n Route::bind('transcription', function ($value) {\n return Activity\\Transcription::uuid($value);\n });\n\n Route::bind('transcriptionProvider', static function (string $scorecardUuid): Models\\TranscriptionProvider {\n return Models\\TranscriptionProvider::where(\n 'uuid',\n Models\\TranscriptionProvider::toOptimized($scorecardUuid)\n )->firstOrFail();\n });\n\n Route::bind('coachingFeedback', function ($value) {\n return CoachingFeedback::uuid($value);\n });\n\n Route::bind('team', function ($value) {\n return Team::uuid($value);\n });\n\n Route::bind('crmTemplate', fn ($value) => CrmTemplate::uuid($value));\n\n Route::bind('crmTemplateField', fn ($value) => CrmTemplateField::uuid($value));\n\n Route::bind('aiScorecard', fn ($value) => AiScorecard::uuid($value));\n\n Route::bind('aiScorecardRule', fn ($value) => AiScorecardRule::uuid($value));\n\n Route::bind('group', function ($value) {\n return Group::uuid($value);\n });\n\n Route::bind('user', function ($value) {\n return User::uuid($value);\n });\n\n Route::bind('comment', function (string $value, \\Illuminate\\Routing\\Route $route) {\n $targetController = $route->getController();\n if (! $targetController instanceof CommentContextInterface) {\n throw new LogicException(\n 'Type hinting comment will require additional effort due to polymorphism.'\n . ' Either implement CommentContextInterface or inject $commentId and process manually',\n );\n }\n\n $commentClass = $targetController::getCommentImplementation();\n\n return $commentClass::uuid($value);\n });\n\n Route::bind('track', function ($value) {\n return Track::uuid($value);\n });\n\n Route::bind('invitation', function ($value) {\n return Invitation::uuid($value);\n });\n\n Route::bind('playbook', function ($value) {\n return Playbook::uuid($value);\n });\n\n Route::bind('category', function ($value) {\n return PlaybookCategory::uuid($value);\n });\n\n Route::bind('coachingSection', function ($value) {\n return CoachingSection::uuid($value);\n });\n\n Route::bind('coachingSectionCriterion', function ($value) {\n return Models\\CoachingSectionCriterion::uuid($value);\n });\n\n Route::bind('playlist', function ($value) {\n return Playlist::uuid($value);\n });\n\n Route::bind('playlistActivity', static fn (string $value) => PlaylistActivity::uuid($value));\n\n Route::bind('playlistTrack', static fn (string $value) => PlaylistActivity::uuid($value));\n\n Route::bind('playlistShare', static fn (string $uuid) => Playlist\\Share::uuid($uuid));\n\n Route::bind('job', function ($value) {\n return JobTitle::uuid($value);\n });\n\n Route::bind('notification', function ($value) {\n return Activity\\AvailabilityNotification::uuid($value);\n });\n\n Route::bind('activityMoment', function ($value) {\n return Activity\\Moment::uuid($value);\n });\n\n Route::bind('search', function ($value) {\n return Activity\\Search::uuid($value);\n });\n\n Route::bind('nudge', static function ($value): Nudge {\n return Nudge::uuid($value);\n });\n\n Route::bind('theme', static function ($value): Models\\PlaybackTheme {\n return Models\\PlaybackTheme::uuid($value);\n });\n\n Route::bind('topic', static function ($value): Models\\PlaybackTheme\\Topic {\n return Models\\PlaybackTheme\\Topic::uuid($value);\n });\n\n Route::bind('topicTrigger', static function ($value): Models\\PlaybackTheme\\TopicTrigger {\n return Models\\PlaybackTheme\\TopicTrigger::uuid($value);\n });\n\n Route::bind('vocabulary', static function (string $id): Models\\Vocabulary {\n return Models\\Vocabulary::uuid($id);\n });\n\n Route::bind('teamDomain', function ($value) {\n return Models\\TeamDomain::uuid($value);\n });\n\n Route::bind('opportunity', function ($value) {\n return Opportunity::uuid($value);\n });\n\n Route::bind('dealRisk', function ($value) {\n return DealRisk::uuid($value);\n });\n\n Route::bind('layout', static fn (string $uuid) => Models\\Crm\\Layout::uuid($uuid));\n\n Route::bind('scorecard', static function (string $scorecardUuid): Models\\Scorecard\\Scorecard {\n return Models\\Scorecard\\Scorecard::where(\n 'uuid',\n Models\\Scorecard\\Scorecard::toOptimized($scorecardUuid)\n )->firstOrFail();\n });\n\n Route::bind('scorecardRule', static function (string $scorecardRuleUuid): Models\\Scorecard\\ScorecardRule {\n return Models\\Scorecard\\ScorecardRule::where(\n 'uuid',\n Models\\Scorecard\\ScorecardRule::toOptimized($scorecardRuleUuid)\n )->firstOrFail();\n });\n\n Route::bind('askAnythingPrompt', function ($value) {\n return Models\\AskAnything\\AskAnythingPrompt::uuid($value);\n });\n }\n\n /**\n * Define the routes for the application.\n *\n * @param \\Illuminate\\Routing\\Router $router\n */\n\n /**\n * Define the \"web\" routes for the application.\n *\n * These routes all receive session state, CSRF protection, etc.\n */\n private function mapWebRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['web'],\n ], static function (Router $router) {\n require base_path('routes/web.php');\n });\n\n return $this;\n }\n\n /**\n * Define the \"Embedded\" routes for the application.\n *\n * These routes depend on a Partitioned cookie,\n * and are accessible only after login.\n */\n private function mapEmbeddedRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['embedded'],\n 'prefix' => 'embedded',\n ], static function (Router $router) {\n require base_path('routes/embedded.php');\n });\n\n return $this;\n }\n\n private function mapWebhookRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['webhook'],\n 'prefix' => 'webhook',\n ], function (Router $router) {\n require base_path('routes/webhook.php');\n });\n\n return $this;\n }\n\n /**\n * Define the \"api\" routes for the application.\n */\n private function mapApiRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['api'],\n 'prefix' => 'api/v1',\n ], function (Router $router) {\n require base_path('routes/api.php');\n });\n\n return $this;\n }\n\n private function mapApiV2Routes(Router $router): self\n {\n $router->group([\n 'middleware' => ['api', 'auth:api'],\n 'prefix' => 'api/v2',\n ], static function (Router $router) {\n require base_path('routes/api_v2.php');\n });\n\n return $this;\n }\n\n private function mapScimRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['scim'],\n 'prefix' => 'scim/v2',\n ], function (Router $router) {\n require base_path('routes/scim.php');\n });\n\n return $this;\n }\n\n private function mapCustomerApiRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['customer-api'],\n 'prefix' => 'customer/api/v1',\n ], function (Router $router) {\n require base_path('routes/customer_api.php');\n });\n\n return $this;\n }\n\n /** Configure the rate limiters for the application. */\n protected function configureRateLimiting(): void\n {\n RateLimiter::for('api', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User || $user instanceof Models\\Partner) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n if (! is_string($subject)) {\n throw new LogicException('Unable to identify subject to rate limit');\n }\n\n return Limit::perMinute(120)->by($subject);\n });\n\n RateLimiter::for('customer-api', static function (Request $request) {\n // Get the token using the same method as the middleware\n $token = null;\n if ($request->bearerToken() !== null) {\n $token = $request->bearerToken();\n } elseif ($request->get('api_token') !== null) {\n $token = $request->get('api_token');\n } elseif ($request->post('api_token') !== null) {\n $token = $request->post('api_token');\n }\n\n // If we have a valid token, get the team ID for rate limiting\n if ($token !== null && strlen($token) === Team::CUSTOMER_API_TOKEN_LENGTH) {\n $tokens = Cache::remember(Team::CUSTOMER_API_TOKENS_CACHE, 60 * 60, static function () {\n return Team::query()->whereNotNull('api_token')->pluck('id', 'api_token');\n });\n\n $hashedToken = hash('sha256', $token);\n if (is_countable($tokens) && ! empty($tokens) && isset($tokens[$hashedToken])) {\n $teamId = $tokens[$hashedToken];\n $team = Team::find($teamId);\n\n if ($team) {\n $subject = $team->getUuid();\n\n // Special rate limit for Funding Circle and Cision.\n if (in_array($subject, [\n '9762611a-fc86-4234-baba-a436de26e774', '1151b5b0-0680-4190-b30c-72649280837d',\n '16b50903-a115-448f-877c-6c01e8b45f5d', 'bfc1c304-83b2-47a3-a7ce-1d28b0e1e90e',\n '447ee473-7dd8-41bc-a702-9322e5b0ec5b'])) {\n // Route-specific rate limiting\n if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {\n return Limit::perMinute(40)->by($subject);\n }\n\n return Limit::perMinute(1000)->by($subject);\n }\n\n // Route-specific rate limiting\n if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {\n return Limit::perMinute(30)->by($subject);\n }\n\n return Limit::perMinute(120)->by($subject);\n }\n }\n }\n\n // Default fallback to IP-based limiting\n return Limit::perMinute(120)->by($request->ip());\n });\n\n RateLimiter::for('conference-consent', static function (Request $request) {\n // Rate limit by IP address for public consent endpoint\n // Allow 10 requests per minute per IP to prevent abuse while allowing legitimate use\n return Limit::perMinute(10)->by($request->ip());\n });\n\n RateLimiter::for('activity-export-shareable-link', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(30)->by($subject);\n });\n\n RateLimiter::for('activity-export', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(10)->by($subject);\n });\n\n RateLimiter::for('transcription-download', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(3)->by($subject);\n });\n }\n}","depth":4,"bounds":{"left":0.13863032,"top":0.1963288,"width":0.3863032,"height":0.8036712},"value":"<?php\n\nnamespace Jiminny\\Providers;\n\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\RateLimiter;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Routing\\Router;\nuse Illuminate\\Foundation\\Support\\Providers\\RouteServiceProvider as ServiceProvider;\nuse Illuminate\\Support\\Facades\\Route;\nuse Jiminny\\Component\\DealRisks\\DealRisk;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Http\\Controllers\\CommentContextInterface;\nuse Jiminny\\Http\\Controllers\\CustomerApi\\CustomerApiController;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Ai\\AiScorecard;\nuse Jiminny\\Models\\Ai\\AiScorecardRule;\nuse Jiminny\\Models\\Ai\\CrmTemplate;\nuse Jiminny\\Models\\Ai\\CrmTemplateField;\nuse Jiminny\\Models\\CoachingFeedback;\nuse Jiminny\\Models\\CoachingSection;\nuse Jiminny\\Models\\Group;\nuse Jiminny\\Models\\Invitation;\nuse Jiminny\\Models\\JobTitle;\nuse Jiminny\\Models\\Nudge;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Models\\Playlist;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Models\\Session;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\Track;\nuse Jiminny\\Models\\User;\n\nclass RouteServiceProvider extends ServiceProvider\n{\n /**\n * Define your route model bindings, pattern filters, etc.\n */\n public function boot(): void\n {\n $this->configureRateLimiting();\n\n $this->routes(function (Router $router) {\n $this\n ->mapHealthRoutes($router)\n ->mapWebhookRoutes($router)\n ->mapApiRoutes($router)\n ->mapApiV2Routes($router)\n ->mapWebRoutes($router)\n ->mapScimRoutes($router)\n ->mapCustomerApiRoutes($router)\n ->mapEmbeddedRoutes($router)\n ;\n });\n $this->defineRouteBindings();\n }\n\n private function mapHealthRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n ], static function (Router $router) {\n require base_path('routes/health.php');\n });\n\n return $this;\n }\n\n /**\n * Define the route model bindings.\n */\n protected function defineRouteBindings()\n {\n Route::pattern('teamSlug', '^[.\\-_a-z0-9]*$');\n Route::pattern('userSlug', '^[.\\-_a-z0-9]*$');\n\n Route::bind('participant', function ($value) {\n return Participant::uuid($value);\n });\n\n Route::bind('participantA', static fn (string $value): Participant => Participant::uuid($value));\n\n Route::bind('participantB', static fn (string $value): Participant => Participant::uuid($value));\n\n Route::bind('connection', function ($value) {\n return Connection::uuid($value);\n });\n\n Route::bind('activity', function ($value) {\n return Activity::uuid($value);\n });\n\n Route::bind('session', function (string $value) {\n return Session::uuid($value);\n });\n\n Route::bind('transcription', function ($value) {\n return Activity\\Transcription::uuid($value);\n });\n\n Route::bind('transcriptionProvider', static function (string $scorecardUuid): Models\\TranscriptionProvider {\n return Models\\TranscriptionProvider::where(\n 'uuid',\n Models\\TranscriptionProvider::toOptimized($scorecardUuid)\n )->firstOrFail();\n });\n\n Route::bind('coachingFeedback', function ($value) {\n return CoachingFeedback::uuid($value);\n });\n\n Route::bind('team', function ($value) {\n return Team::uuid($value);\n });\n\n Route::bind('crmTemplate', fn ($value) => CrmTemplate::uuid($value));\n\n Route::bind('crmTemplateField', fn ($value) => CrmTemplateField::uuid($value));\n\n Route::bind('aiScorecard', fn ($value) => AiScorecard::uuid($value));\n\n Route::bind('aiScorecardRule', fn ($value) => AiScorecardRule::uuid($value));\n\n Route::bind('group', function ($value) {\n return Group::uuid($value);\n });\n\n Route::bind('user', function ($value) {\n return User::uuid($value);\n });\n\n Route::bind('comment', function (string $value, \\Illuminate\\Routing\\Route $route) {\n $targetController = $route->getController();\n if (! $targetController instanceof CommentContextInterface) {\n throw new LogicException(\n 'Type hinting comment will require additional effort due to polymorphism.'\n . ' Either implement CommentContextInterface or inject $commentId and process manually',\n );\n }\n\n $commentClass = $targetController::getCommentImplementation();\n\n return $commentClass::uuid($value);\n });\n\n Route::bind('track', function ($value) {\n return Track::uuid($value);\n });\n\n Route::bind('invitation', function ($value) {\n return Invitation::uuid($value);\n });\n\n Route::bind('playbook', function ($value) {\n return Playbook::uuid($value);\n });\n\n Route::bind('category', function ($value) {\n return PlaybookCategory::uuid($value);\n });\n\n Route::bind('coachingSection', function ($value) {\n return CoachingSection::uuid($value);\n });\n\n Route::bind('coachingSectionCriterion', function ($value) {\n return Models\\CoachingSectionCriterion::uuid($value);\n });\n\n Route::bind('playlist', function ($value) {\n return Playlist::uuid($value);\n });\n\n Route::bind('playlistActivity', static fn (string $value) => PlaylistActivity::uuid($value));\n\n Route::bind('playlistTrack', static fn (string $value) => PlaylistActivity::uuid($value));\n\n Route::bind('playlistShare', static fn (string $uuid) => Playlist\\Share::uuid($uuid));\n\n Route::bind('job', function ($value) {\n return JobTitle::uuid($value);\n });\n\n Route::bind('notification', function ($value) {\n return Activity\\AvailabilityNotification::uuid($value);\n });\n\n Route::bind('activityMoment', function ($value) {\n return Activity\\Moment::uuid($value);\n });\n\n Route::bind('search', function ($value) {\n return Activity\\Search::uuid($value);\n });\n\n Route::bind('nudge', static function ($value): Nudge {\n return Nudge::uuid($value);\n });\n\n Route::bind('theme', static function ($value): Models\\PlaybackTheme {\n return Models\\PlaybackTheme::uuid($value);\n });\n\n Route::bind('topic', static function ($value): Models\\PlaybackTheme\\Topic {\n return Models\\PlaybackTheme\\Topic::uuid($value);\n });\n\n Route::bind('topicTrigger', static function ($value): Models\\PlaybackTheme\\TopicTrigger {\n return Models\\PlaybackTheme\\TopicTrigger::uuid($value);\n });\n\n Route::bind('vocabulary', static function (string $id): Models\\Vocabulary {\n return Models\\Vocabulary::uuid($id);\n });\n\n Route::bind('teamDomain', function ($value) {\n return Models\\TeamDomain::uuid($value);\n });\n\n Route::bind('opportunity', function ($value) {\n return Opportunity::uuid($value);\n });\n\n Route::bind('dealRisk', function ($value) {\n return DealRisk::uuid($value);\n });\n\n Route::bind('layout', static fn (string $uuid) => Models\\Crm\\Layout::uuid($uuid));\n\n Route::bind('scorecard', static function (string $scorecardUuid): Models\\Scorecard\\Scorecard {\n return Models\\Scorecard\\Scorecard::where(\n 'uuid',\n Models\\Scorecard\\Scorecard::toOptimized($scorecardUuid)\n )->firstOrFail();\n });\n\n Route::bind('scorecardRule', static function (string $scorecardRuleUuid): Models\\Scorecard\\ScorecardRule {\n return Models\\Scorecard\\ScorecardRule::where(\n 'uuid',\n Models\\Scorecard\\ScorecardRule::toOptimized($scorecardRuleUuid)\n )->firstOrFail();\n });\n\n Route::bind('askAnythingPrompt', function ($value) {\n return Models\\AskAnything\\AskAnythingPrompt::uuid($value);\n });\n }\n\n /**\n * Define the routes for the application.\n *\n * @param \\Illuminate\\Routing\\Router $router\n */\n\n /**\n * Define the \"web\" routes for the application.\n *\n * These routes all receive session state, CSRF protection, etc.\n */\n private function mapWebRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['web'],\n ], static function (Router $router) {\n require base_path('routes/web.php');\n });\n\n return $this;\n }\n\n /**\n * Define the \"Embedded\" routes for the application.\n *\n * These routes depend on a Partitioned cookie,\n * and are accessible only after login.\n */\n private function mapEmbeddedRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['embedded'],\n 'prefix' => 'embedded',\n ], static function (Router $router) {\n require base_path('routes/embedded.php');\n });\n\n return $this;\n }\n\n private function mapWebhookRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['webhook'],\n 'prefix' => 'webhook',\n ], function (Router $router) {\n require base_path('routes/webhook.php');\n });\n\n return $this;\n }\n\n /**\n * Define the \"api\" routes for the application.\n */\n private function mapApiRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['api'],\n 'prefix' => 'api/v1',\n ], function (Router $router) {\n require base_path('routes/api.php');\n });\n\n return $this;\n }\n\n private function mapApiV2Routes(Router $router): self\n {\n $router->group([\n 'middleware' => ['api', 'auth:api'],\n 'prefix' => 'api/v2',\n ], static function (Router $router) {\n require base_path('routes/api_v2.php');\n });\n\n return $this;\n }\n\n private function mapScimRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['scim'],\n 'prefix' => 'scim/v2',\n ], function (Router $router) {\n require base_path('routes/scim.php');\n });\n\n return $this;\n }\n\n private function mapCustomerApiRoutes(Router $router): self\n {\n $router->group([\n 'namespace' => $this->namespace,\n 'middleware' => ['customer-api'],\n 'prefix' => 'customer/api/v1',\n ], function (Router $router) {\n require base_path('routes/customer_api.php');\n });\n\n return $this;\n }\n\n /** Configure the rate limiters for the application. */\n protected function configureRateLimiting(): void\n {\n RateLimiter::for('api', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User || $user instanceof Models\\Partner) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n if (! is_string($subject)) {\n throw new LogicException('Unable to identify subject to rate limit');\n }\n\n return Limit::perMinute(120)->by($subject);\n });\n\n RateLimiter::for('customer-api', static function (Request $request) {\n // Get the token using the same method as the middleware\n $token = null;\n if ($request->bearerToken() !== null) {\n $token = $request->bearerToken();\n } elseif ($request->get('api_token') !== null) {\n $token = $request->get('api_token');\n } elseif ($request->post('api_token') !== null) {\n $token = $request->post('api_token');\n }\n\n // If we have a valid token, get the team ID for rate limiting\n if ($token !== null && strlen($token) === Team::CUSTOMER_API_TOKEN_LENGTH) {\n $tokens = Cache::remember(Team::CUSTOMER_API_TOKENS_CACHE, 60 * 60, static function () {\n return Team::query()->whereNotNull('api_token')->pluck('id', 'api_token');\n });\n\n $hashedToken = hash('sha256', $token);\n if (is_countable($tokens) && ! empty($tokens) && isset($tokens[$hashedToken])) {\n $teamId = $tokens[$hashedToken];\n $team = Team::find($teamId);\n\n if ($team) {\n $subject = $team->getUuid();\n\n // Special rate limit for Funding Circle and Cision.\n if (in_array($subject, [\n '9762611a-fc86-4234-baba-a436de26e774', '1151b5b0-0680-4190-b30c-72649280837d',\n '16b50903-a115-448f-877c-6c01e8b45f5d', 'bfc1c304-83b2-47a3-a7ce-1d28b0e1e90e',\n '447ee473-7dd8-41bc-a702-9322e5b0ec5b'])) {\n // Route-specific rate limiting\n if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {\n return Limit::perMinute(40)->by($subject);\n }\n\n return Limit::perMinute(1000)->by($subject);\n }\n\n // Route-specific rate limiting\n if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {\n return Limit::perMinute(30)->by($subject);\n }\n\n return Limit::perMinute(120)->by($subject);\n }\n }\n }\n\n // Default fallback to IP-based limiting\n return Limit::perMinute(120)->by($request->ip());\n });\n\n RateLimiter::for('conference-consent', static function (Request $request) {\n // Rate limit by IP address for public consent endpoint\n // Allow 10 requests per minute per IP to prevent abuse while allowing legitimate use\n return Limit::perMinute(10)->by($request->ip());\n });\n\n RateLimiter::for('activity-export-shareable-link', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(30)->by($subject);\n });\n\n RateLimiter::for('activity-export', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(10)->by($subject);\n });\n\n RateLimiter::for('transcription-download', static function (Request $request) {\n $user = $request->user();\n\n if ($user instanceof User) {\n $subject = $user->getUuid();\n } else {\n $subject = $request->ip();\n }\n\n return Limit::perMinute(3)->by($subject);\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.9527925,"top":0.10055866,"width":0.007978723,"height":0.015163607},"role_description":"text"},{"role":"AXStaticText","text":"14","depth":4,"bounds":{"left":0.96276593,"top":0.10055866,"width":0.009640957,"height":0.015163607},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09896249,"width":0.00731383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09896249,"width":0.006981383,"height":0.018355945},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"{\n \"name\": \"jiminny/app\",\n \"description\": \"The Jiminny Platform.\",\n \"keywords\": [\n \"training\",\n \"salesforce\",\n \"conference\"\n ],\n \"license\": \"MIT\",\n \"type\": \"project\",\n \"require\": {\n \"php\": \"^8.3\",\n \"ext-ctype\": \"*\",\n \"ext-curl\": \"*\",\n \"ext-date\": \"*\",\n \"ext-dom\": \"*\",\n \"ext-fileinfo\": \"*\",\n \"ext-filter\": \"*\",\n \"ext-gd\": \"*\",\n \"ext-gmp\": \"*\",\n \"ext-hash\": \"*\",\n \"ext-iconv\": \"*\",\n \"ext-igbinary\": \"*\",\n \"ext-imagick\": \"*\",\n \"ext-intl\": \"*\",\n \"ext-json\": \"*\",\n \"ext-libxml\": \"*\",\n \"ext-mailparse\": \"*\",\n \"ext-mbstring\": \"*\",\n \"ext-mysqlnd\": \"*\",\n \"ext-openssl\": \"*\",\n \"ext-pcntl\": \"*\",\n \"ext-pcre\": \"*\",\n \"ext-pdo\": \"*\",\n \"ext-pdo_mysql\": \"*\",\n \"ext-phar\": \"*\",\n \"ext-phpiredis\": \"*\",\n \"ext-posix\": \"*\",\n \"ext-readline\": \"*\",\n \"ext-redis\": \"*\",\n \"ext-reflection\": \"*\",\n \"ext-session\": \"*\",\n \"ext-simplexml\": \"*\",\n \"ext-sockets\": \"*\",\n \"ext-spl\": \"*\",\n \"ext-tokenizer\": \"*\",\n \"ext-xml\": \"*\",\n \"ext-xmlreader\": \"*\",\n \"ext-xmlwriter\": \"*\",\n \"ext-zend-opcache\": \"*\",\n \"ext-zip\": \"*\",\n \"ext-zlib\": \"*\",\n \"lib-curl\": \"*\",\n \"lib-curl-openssl\": \"*\",\n \"lib-curl-zlib\": \"*\",\n \"lib-date-timelib\": \"*\",\n \"lib-date-zoneinfo\": \"*\",\n \"lib-fileinfo-libmagic\": \"*\",\n \"lib-gd\": \"*\",\n \"lib-gd-freetype\": \"*\",\n \"lib-gd-libjpeg\": \"*\",\n \"lib-gd-libpng\": \"*\",\n \"lib-gmp\": \"*\",\n \"lib-icu\": \"*\",\n \"lib-icu-cldr\": \"*\",\n \"lib-icu-unicode\": \"*\",\n \"lib-imagick-imagemagick\": \"*\",\n \"lib-libxml\": \"*\",\n \"lib-mbstring-libmbfl\": \"*\",\n \"lib-mbstring-oniguruma\": \"*\",\n \"lib-openssl\": \"*\",\n \"lib-pcre\": \"*\",\n \"lib-pcre-unicode\": \"*\",\n \"lib-zip-libzip\": \"*\",\n \"lib-zlib\": \"*\",\n \"24slides/laravel-saml2\": \"^2.4\",\n \"adam-paterson/oauth2-slack\": \"^1.1\",\n \"asimlqt/php-google-spreadsheet-client\": \"^3.0\",\n \"aws/aws-sdk-php\": \"^3.368\",\n \"aws/aws-sdk-php-laravel\": \"^3.10\",\n \"bepsvpt/secure-headers\": \"^9.0\",\n \"chadhutchins/oauth2-slack\": \"^1.2\",\n \"chaseconey/laravel-datadog-helper\": \"^1.2\",\n \"chrisyue/php-m3u8\": \"4.0.3\",\n \"daniti/oauth2-pipedrive\": \"dev-master\",\n \"devio/pipedrive\": \"^2.6\",\n \"doctrine/dbal\": \"^4.0\",\n \"elasticsearch/elasticsearch\": \"^7.11\",\n \"erusev/parsedown\": \"^1.7\",\n \"fakerphp/faker\": \"^1.23\",\n \"firebase/php-jwt\": \"^7.0\",\n \"flipboxdigital/oauth2-hubspot\": \"1.0.1\",\n \"giggsey/libphonenumber-for-php\": \"^8.12\",\n \"google/apiclient\": \"^2.19\",\n \"google/apiclient-services\": \"~0.360\",\n \"google/apps-meet\": \"^0.5.1\",\n \"guzzlehttp/guzzle\": \"^7.8\",\n \"guzzlehttp/psr7\": \"^2.6\",\n \"halaxa/json-machine\": \"^1.2\",\n \"html2text/html2text\": \"^4.3\",\n \"hubspot/api-client\": \"~5.0.0\",\n \"hubspot/hubspot-php\": \"^5.2.0\",\n \"intercom/intercom-php\": \"^4.5\",\n \"intervention/image\": \"^3.4\",\n \"jakeasmith/http_build_url\": \"^1.0\",\n \"jdavidbakr/cloudfront-proxies\": \"^1.7\",\n \"jeremykendall/php-domain-parser\": \"^6.3\",\n \"jiminny/oauth2-aircall\": \"dev-master\",\n \"jiminny/oauth2-bullhorn\": \"^0.2.0\",\n \"jiminny/oauth2-dialpad\": \"dev-master\",\n \"jiminny/oauth2-salesloft\": \"dev-master\",\n \"jolicode/slack-php-api\": \"^4.5.0\",\n \"kalnoy/nestedset\": \"*\",\n \"laravel/framework\": \"^12.28\",\n \"laravel/helpers\": \"^1.7\",\n \"laravel/passport\": \"^13.0\",\n \"laravel/slack-notification-channel\": \"^3.4\",\n \"laravel/tinker\": \"^2.10.1\",\n \"laravel/ui\": \"^4.6\",\n \"laravolt/avatar\": \"^6.1\",\n \"league/flysystem\": \"^3.0\",\n \"league/flysystem-aws-s3-v3\": \"^3.0\",\n \"league/fractal\": \"*\",\n \"league/oauth2-client\": \"^2.7\",\n \"league/oauth2-google\": \"^4.0\",\n \"league/oauth2-linkedin\": \"^5.1\",\n \"league/oauth2-server\": \"^9.2\",\n \"league/statsd\": \"^2.0\",\n \"markrogoyski/math-php\": \"^2.7.0\",\n \"microsoft/microsoft-graph\": \"^2.51\",\n \"monolog/monolog\": \"^3.0\",\n \"nesbot/carbon\": \"^3.8\",\n \"nette/caching\": \"*\",\n \"phlib/sms-length\": \"^2.0\",\n \"php-ffmpeg/php-ffmpeg\": \"^1.2\",\n \"php-http/client-common\": \"^2.7\",\n \"php-http/curl-client\": \"^2.3\",\n \"php-http/httplug\": \"^2.2\",\n \"php-http/message\": \"^1.16\",\n \"phpseclib/phpseclib\": \"^3.0.36\",\n \"propaganistas/laravel-phone\": \"^5.3\",\n \"psr/cache\": \"^3.0\",\n \"psr/http-message\": \"^2.0\",\n \"psr/log\": \"^3.0\",\n \"psr/simple-cache\": \"^3.0\",\n \"pusher/pusher-php-server\": \"7.2.3\",\n \"ramsey/uuid\": \"^4.2\",\n \"ringcentral/ringcentral-php\": \"3.0.0\",\n \"rmccue/requests\": \"^2.0\",\n \"ruflin/elastica\": \"^7.1.1\",\n \"santigarcor/laratrust\": \"^8.4\",\n \"sentry/sentry\": \"4.13.0\",\n \"sentry/sentry-laravel\": \"~4.13.0\",\n \"shiftonelabs/laravel-sqs-fifo-queue\": \"^3.0\",\n \"spatie/fractalistic\": \"^2.9\",\n \"spatie/laravel-fractal\": \"^6.3\",\n \"spatie/laravel-ignition\": \"^2.9\",\n \"spatie/laravel-webhook-server\": \"^3.8\",\n \"staudenmeir/belongs-to-through\": \"^2.17\",\n \"stevenmaguire/oauth2-salesforce\": \"^2.0\",\n \"symfony/cache\": \"^7.2\",\n \"symfony/console\": \"^7.2\",\n \"symfony/css-selector\": \"^7.2\",\n \"symfony/debug\": \"^4.4\",\n \"symfony/dom-crawler\": \"^7.2\",\n \"symfony/expression-language\": \"^7.2\",\n \"symfony/finder\": \"^7.2\",\n \"symfony/http-client\": \"^7.3\",\n \"symfony/http-foundation\": \"^7.2\",\n \"symfony/http-kernel\": \"^7.2\",\n \"symfony/postmark-mailer\": \"^7.3\",\n \"symfony/process\": \"^7.3\",\n \"symfony/property-access\": \"^7.2\",\n \"symfony/psr-http-message-bridge\": \"^7.0\",\n \"symfony/var-dumper\": \"^7.2\",\n \"symfony/workflow\": \"^7.2\",\n \"tecnickcom/tcpdf\": \"^6.11\",\n \"thenetworg/oauth2-azure\": \"dev-master\",\n \"tmannherz/oauth2-ringcentral\": \"dev-master\",\n \"twilio/sdk\": \"^8.3\",\n \"vanderlee/php-sentence\": \"^1.0\",\n \"vinkla/hashids\": \"^13.0\",\n \"vlucas/phpdotenv\": \"^5.4\",\n \"wildbit/postmark-php\": \"^6.0\",\n \"willdurand/email-reply-parser\": \"^2.8\",\n \"zbateson/mail-mime-parser\": \"^3.0.4\"\n },\n \"require-dev\": {\n \"barryvdh/laravel-debugbar\": \"^3.15\",\n \"barryvdh/laravel-ide-helper\": \"^3.5\",\n \"brianium/paratest\": \"^7.5\",\n \"browserstack/browserstack-local\": \"^1.1.0\",\n \"filp/whoops\": \"^2.9\",\n \"friendsofphp/php-cs-fixer\": \"^3.66\",\n \"infection/infection\": \"^0.29.14\",\n \"jasonmccreary/laravel-test-assertions\": \"^2.5\",\n \"larastan/larastan\": \"^3.1\",\n \"maglnet/composer-require-checker\": \"^4.8\",\n \"mockery/mockery\": \"^1.6\",\n \"nunomaduro/collision\": \"^8.6\",\n \"phpstan/phpstan\": \"^2.1\",\n \"phpunit/phpunit\": \"^11.5.50\",\n \"symfony/phpunit-bridge\": \"^7.0\",\n \"vimeo/psalm\": \"^6.5.0\"\n },\n \"autoload\": {\n \"classmap\": [\n \"database\"\n ],\n \"psr-4\": {\n \"Jiminny\\\\\": \"app/\",\n \"Tests\\\\\": \"tests/\",\n \"Database\\\\Factories\\\\\": \"database/factories/\",\n \"Database\\\\Seeders\\\\\": \"database/seeders/\",\n \"Microsoft\\\\Graph\\\\Generated\\\\Models\\\\\": \"app/Services/MeetingGenerator/Overrides/Microsoft/Graph/Generated/Models/\"\n },\n \"files\": [\n \"app/helpers.php\"\n ]\n },\n \"autoload-dev\": {\n \"classmap\": [\n \"tests/TestCase.php\"\n ],\n \"psr-4\": {\n \"Jiminny\\\\\": \"app/\",\n \"Tests\\\\\": \"tests/\"\n }\n },\n \"scripts\": {\n \"post-root-package-install\": [\n \"php -r \\\"file_exists('.env') || copy('.env.example', '.env');\\\"\"\n ],\n \"post-create-project-cmd\": [\n \"php artisan key:generate --ansi\"\n ],\n \"post-install-cmd\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postInstall\"\n ],\n \"post-update-cmd\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postUpdate\",\n \"php artisan ide-helper:generate\",\n \"php artisan ide-helper:meta\",\n \"@php artisan vendor:publish --tag=laravel-assets --ansi --force\"\n ],\n \"post-autoload-dump\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postAutoloadDump\",\n \"@php artisan package:discover --ansi\"\n ]\n },\n \"config\": {\n \"preferred-install\": \"dist\",\n \"sort-packages\": true,\n \"optimize-autoloader\": true,\n \"allow-plugins\": {\n \"infection/extension-installer\": true,\n \"php-http/discovery\": true,\n \"tbachert/spi\": true\n }\n },\n \"extra\": {\n \"laravel\": {\n \"dont-discover\": [\n \"laravel/dusk\"\n ]\n },\n \"metasyntactical/composer-plugin-license-check\": {\n \"whitelist\": [],\n \"blacklist\": [\n \"AGPL\"\n ]\n }\n },\n \"repositories\": [\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/PHP-FFMpeg/BinaryDriver.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-salesloft.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-aircall.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-pipedrive.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-ringcentral\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-dialpad.git\"\n }\n ],\n \"prefer-stable\": true\n}","depth":4,"value":"{\n \"name\": \"jiminny/app\",\n \"description\": \"The Jiminny Platform.\",\n \"keywords\": [\n \"training\",\n \"salesforce\",\n \"conference\"\n ],\n \"license\": \"MIT\",\n \"type\": \"project\",\n \"require\": {\n \"php\": \"^8.3\",\n \"ext-ctype\": \"*\",\n \"ext-curl\": \"*\",\n \"ext-date\": \"*\",\n \"ext-dom\": \"*\",\n \"ext-fileinfo\": \"*\",\n \"ext-filter\": \"*\",\n \"ext-gd\": \"*\",\n \"ext-gmp\": \"*\",\n \"ext-hash\": \"*\",\n \"ext-iconv\": \"*\",\n \"ext-igbinary\": \"*\",\n \"ext-imagick\": \"*\",\n \"ext-intl\": \"*\",\n \"ext-json\": \"*\",\n \"ext-libxml\": \"*\",\n \"ext-mailparse\": \"*\",\n \"ext-mbstring\": \"*\",\n \"ext-mysqlnd\": \"*\",\n \"ext-openssl\": \"*\",\n \"ext-pcntl\": \"*\",\n \"ext-pcre\": \"*\",\n \"ext-pdo\": \"*\",\n \"ext-pdo_mysql\": \"*\",\n \"ext-phar\": \"*\",\n \"ext-phpiredis\": \"*\",\n \"ext-posix\": \"*\",\n \"ext-readline\": \"*\",\n \"ext-redis\": \"*\",\n \"ext-reflection\": \"*\",\n \"ext-session\": \"*\",\n \"ext-simplexml\": \"*\",\n \"ext-sockets\": \"*\",\n \"ext-spl\": \"*\",\n \"ext-tokenizer\": \"*\",\n \"ext-xml\": \"*\",\n \"ext-xmlreader\": \"*\",\n \"ext-xmlwriter\": \"*\",\n \"ext-zend-opcache\": \"*\",\n \"ext-zip\": \"*\",\n \"ext-zlib\": \"*\",\n \"lib-curl\": \"*\",\n \"lib-curl-openssl\": \"*\",\n \"lib-curl-zlib\": \"*\",\n \"lib-date-timelib\": \"*\",\n \"lib-date-zoneinfo\": \"*\",\n \"lib-fileinfo-libmagic\": \"*\",\n \"lib-gd\": \"*\",\n \"lib-gd-freetype\": \"*\",\n \"lib-gd-libjpeg\": \"*\",\n \"lib-gd-libpng\": \"*\",\n \"lib-gmp\": \"*\",\n \"lib-icu\": \"*\",\n \"lib-icu-cldr\": \"*\",\n \"lib-icu-unicode\": \"*\",\n \"lib-imagick-imagemagick\": \"*\",\n \"lib-libxml\": \"*\",\n \"lib-mbstring-libmbfl\": \"*\",\n \"lib-mbstring-oniguruma\": \"*\",\n \"lib-openssl\": \"*\",\n \"lib-pcre\": \"*\",\n \"lib-pcre-unicode\": \"*\",\n \"lib-zip-libzip\": \"*\",\n \"lib-zlib\": \"*\",\n \"24slides/laravel-saml2\": \"^2.4\",\n \"adam-paterson/oauth2-slack\": \"^1.1\",\n \"asimlqt/php-google-spreadsheet-client\": \"^3.0\",\n \"aws/aws-sdk-php\": \"^3.368\",\n \"aws/aws-sdk-php-laravel\": \"^3.10\",\n \"bepsvpt/secure-headers\": \"^9.0\",\n \"chadhutchins/oauth2-slack\": \"^1.2\",\n \"chaseconey/laravel-datadog-helper\": \"^1.2\",\n \"chrisyue/php-m3u8\": \"4.0.3\",\n \"daniti/oauth2-pipedrive\": \"dev-master\",\n \"devio/pipedrive\": \"^2.6\",\n \"doctrine/dbal\": \"^4.0\",\n \"elasticsearch/elasticsearch\": \"^7.11\",\n \"erusev/parsedown\": \"^1.7\",\n \"fakerphp/faker\": \"^1.23\",\n \"firebase/php-jwt\": \"^7.0\",\n \"flipboxdigital/oauth2-hubspot\": \"1.0.1\",\n \"giggsey/libphonenumber-for-php\": \"^8.12\",\n \"google/apiclient\": \"^2.19\",\n \"google/apiclient-services\": \"~0.360\",\n \"google/apps-meet\": \"^0.5.1\",\n \"guzzlehttp/guzzle\": \"^7.8\",\n \"guzzlehttp/psr7\": \"^2.6\",\n \"halaxa/json-machine\": \"^1.2\",\n \"html2text/html2text\": \"^4.3\",\n \"hubspot/api-client\": \"~5.0.0\",\n \"hubspot/hubspot-php\": \"^5.2.0\",\n \"intercom/intercom-php\": \"^4.5\",\n \"intervention/image\": \"^3.4\",\n \"jakeasmith/http_build_url\": \"^1.0\",\n \"jdavidbakr/cloudfront-proxies\": \"^1.7\",\n \"jeremykendall/php-domain-parser\": \"^6.3\",\n \"jiminny/oauth2-aircall\": \"dev-master\",\n \"jiminny/oauth2-bullhorn\": \"^0.2.0\",\n \"jiminny/oauth2-dialpad\": \"dev-master\",\n \"jiminny/oauth2-salesloft\": \"dev-master\",\n \"jolicode/slack-php-api\": \"^4.5.0\",\n \"kalnoy/nestedset\": \"*\",\n \"laravel/framework\": \"^12.28\",\n \"laravel/helpers\": \"^1.7\",\n \"laravel/passport\": \"^13.0\",\n \"laravel/slack-notification-channel\": \"^3.4\",\n \"laravel/tinker\": \"^2.10.1\",\n \"laravel/ui\": \"^4.6\",\n \"laravolt/avatar\": \"^6.1\",\n \"league/flysystem\": \"^3.0\",\n \"league/flysystem-aws-s3-v3\": \"^3.0\",\n \"league/fractal\": \"*\",\n \"league/oauth2-client\": \"^2.7\",\n \"league/oauth2-google\": \"^4.0\",\n \"league/oauth2-linkedin\": \"^5.1\",\n \"league/oauth2-server\": \"^9.2\",\n \"league/statsd\": \"^2.0\",\n \"markrogoyski/math-php\": \"^2.7.0\",\n \"microsoft/microsoft-graph\": \"^2.51\",\n \"monolog/monolog\": \"^3.0\",\n \"nesbot/carbon\": \"^3.8\",\n \"nette/caching\": \"*\",\n \"phlib/sms-length\": \"^2.0\",\n \"php-ffmpeg/php-ffmpeg\": \"^1.2\",\n \"php-http/client-common\": \"^2.7\",\n \"php-http/curl-client\": \"^2.3\",\n \"php-http/httplug\": \"^2.2\",\n \"php-http/message\": \"^1.16\",\n \"phpseclib/phpseclib\": \"^3.0.36\",\n \"propaganistas/laravel-phone\": \"^5.3\",\n \"psr/cache\": \"^3.0\",\n \"psr/http-message\": \"^2.0\",\n \"psr/log\": \"^3.0\",\n \"psr/simple-cache\": \"^3.0\",\n \"pusher/pusher-php-server\": \"7.2.3\",\n \"ramsey/uuid\": \"^4.2\",\n \"ringcentral/ringcentral-php\": \"3.0.0\",\n \"rmccue/requests\": \"^2.0\",\n \"ruflin/elastica\": \"^7.1.1\",\n \"santigarcor/laratrust\": \"^8.4\",\n \"sentry/sentry\": \"4.13.0\",\n \"sentry/sentry-laravel\": \"~4.13.0\",\n \"shiftonelabs/laravel-sqs-fifo-queue\": \"^3.0\",\n \"spatie/fractalistic\": \"^2.9\",\n \"spatie/laravel-fractal\": \"^6.3\",\n \"spatie/laravel-ignition\": \"^2.9\",\n \"spatie/laravel-webhook-server\": \"^3.8\",\n \"staudenmeir/belongs-to-through\": \"^2.17\",\n \"stevenmaguire/oauth2-salesforce\": \"^2.0\",\n \"symfony/cache\": \"^7.2\",\n \"symfony/console\": \"^7.2\",\n \"symfony/css-selector\": \"^7.2\",\n \"symfony/debug\": \"^4.4\",\n \"symfony/dom-crawler\": \"^7.2\",\n \"symfony/expression-language\": \"^7.2\",\n \"symfony/finder\": \"^7.2\",\n \"symfony/http-client\": \"^7.3\",\n \"symfony/http-foundation\": \"^7.2\",\n \"symfony/http-kernel\": \"^7.2\",\n \"symfony/postmark-mailer\": \"^7.3\",\n \"symfony/process\": \"^7.3\",\n \"symfony/property-access\": \"^7.2\",\n \"symfony/psr-http-message-bridge\": \"^7.0\",\n \"symfony/var-dumper\": \"^7.2\",\n \"symfony/workflow\": \"^7.2\",\n \"tecnickcom/tcpdf\": \"^6.11\",\n \"thenetworg/oauth2-azure\": \"dev-master\",\n \"tmannherz/oauth2-ringcentral\": \"dev-master\",\n \"twilio/sdk\": \"^8.3\",\n \"vanderlee/php-sentence\": \"^1.0\",\n \"vinkla/hashids\": \"^13.0\",\n \"vlucas/phpdotenv\": \"^5.4\",\n \"wildbit/postmark-php\": \"^6.0\",\n \"willdurand/email-reply-parser\": \"^2.8\",\n \"zbateson/mail-mime-parser\": \"^3.0.4\"\n },\n \"require-dev\": {\n \"barryvdh/laravel-debugbar\": \"^3.15\",\n \"barryvdh/laravel-ide-helper\": \"^3.5\",\n \"brianium/paratest\": \"^7.5\",\n \"browserstack/browserstack-local\": \"^1.1.0\",\n \"filp/whoops\": \"^2.9\",\n \"friendsofphp/php-cs-fixer\": \"^3.66\",\n \"infection/infection\": \"^0.29.14\",\n \"jasonmccreary/laravel-test-assertions\": \"^2.5\",\n \"larastan/larastan\": \"^3.1\",\n \"maglnet/composer-require-checker\": \"^4.8\",\n \"mockery/mockery\": \"^1.6\",\n \"nunomaduro/collision\": \"^8.6\",\n \"phpstan/phpstan\": \"^2.1\",\n \"phpunit/phpunit\": \"^11.5.50\",\n \"symfony/phpunit-bridge\": \"^7.0\",\n \"vimeo/psalm\": \"^6.5.0\"\n },\n \"autoload\": {\n \"classmap\": [\n \"database\"\n ],\n \"psr-4\": {\n \"Jiminny\\\\\": \"app/\",\n \"Tests\\\\\": \"tests/\",\n \"Database\\\\Factories\\\\\": \"database/factories/\",\n \"Database\\\\Seeders\\\\\": \"database/seeders/\",\n \"Microsoft\\\\Graph\\\\Generated\\\\Models\\\\\": \"app/Services/MeetingGenerator/Overrides/Microsoft/Graph/Generated/Models/\"\n },\n \"files\": [\n \"app/helpers.php\"\n ]\n },\n \"autoload-dev\": {\n \"classmap\": [\n \"tests/TestCase.php\"\n ],\n \"psr-4\": {\n \"Jiminny\\\\\": \"app/\",\n \"Tests\\\\\": \"tests/\"\n }\n },\n \"scripts\": {\n \"post-root-package-install\": [\n \"php -r \\\"file_exists('.env') || copy('.env.example', '.env');\\\"\"\n ],\n \"post-create-project-cmd\": [\n \"php artisan key:generate --ansi\"\n ],\n \"post-install-cmd\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postInstall\"\n ],\n \"post-update-cmd\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postUpdate\",\n \"php artisan ide-helper:generate\",\n \"php artisan ide-helper:meta\",\n \"@php artisan vendor:publish --tag=laravel-assets --ansi --force\"\n ],\n \"post-autoload-dump\": [\n \"Illuminate\\\\Foundation\\\\ComposerScripts::postAutoloadDump\",\n \"@php artisan package:discover --ansi\"\n ]\n },\n \"config\": {\n \"preferred-install\": \"dist\",\n \"sort-packages\": true,\n \"optimize-autoloader\": true,\n \"allow-plugins\": {\n \"infection/extension-installer\": true,\n \"php-http/discovery\": true,\n \"tbachert/spi\": true\n }\n },\n \"extra\": {\n \"laravel\": {\n \"dont-discover\": [\n \"laravel/dusk\"\n ]\n },\n \"metasyntactical/composer-plugin-license-check\": {\n \"whitelist\": [],\n \"blacklist\": [\n \"AGPL\"\n ]\n }\n },\n \"repositories\": [\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/PHP-FFMpeg/BinaryDriver.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-salesloft.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-aircall.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-pipedrive.git\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-ringcentral\"\n },\n {\n \"type\": \"vcs\",\n \"url\": \"https://github.com/jiminny/oauth2-dialpad.git\"\n }\n ],\n \"prefer-stable\": true\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Install","depth":3,"bounds":{"left":0.90957445,"top":0.07821229,"width":0.013297873,"height":0.013567438},"help_text":"Installs packages from composer.json, taking account of composer.lock","role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Update","depth":3,"bounds":{"left":0.9281915,"top":0.07821229,"width":0.016289894,"height":0.013567438},"help_text":"Installs latest appropriate versions of packages from composer.json","role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Show log","depth":3,"bounds":{"left":0.94980055,"top":0.07821229,"width":0.020279255,"height":0.013567438},"help_text":"Show log of Composer-related actions","role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Configuration, folder","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Console, folder","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Commands, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Activities, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Analytics, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Calendars, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Crm, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Dev, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Dialers, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"DTOs, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Elasticsearch, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"EngagementStats, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"GeckoExport, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Livestream, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Mailboxes, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Migrate, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackThemes, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Playbooks, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Playlists, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Postmark, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Reports, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsRetentionPolicyCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsSendCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"CreateMockAskJiminnyReportResultCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"DeleteReportCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"GenerateMarketingReport.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"Team.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"Usage.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Teams, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Tracks, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Users, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Vocabulary, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Zoom, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedbacksUpdateEsActivities.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Command.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"CreateDatabaseUsers.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"DatabaseTableCount.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"DeleteOldAiCrmNotesCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"DeleteS3LeftoversCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"DevPostmanCommand.php, final class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"DiarizeViaAiParticipantIdentificationCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"EncryptTokensCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"EngagementStatsRegenerateCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlagsHelper.php","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"FixCrossTenantIssues.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"FlushRolesPermissionsCache.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"GenerateInternalWebhookToken.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"GroupSetDefaultLanguageCommand.php, final class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"HelperTruncateCoachingTables.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"HubspotJournalPollingCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"HubspotWebhookServiceCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"ImportRecording.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"ImportUsersFromCsvFile.php, final class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"IterateUsersCommand.php, abstract class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyCacheClearCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyDebugCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"JiminnySetEncryptedTokenManagerModeCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyTokenInfoCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"MakeSlackLiveCoachingChatNotesOn.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"ManageScimForTeam.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"MarkBranchForEnvironmentPipelineCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"MuteOrganizerChannel.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"PhpApm.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"PurgeConferences.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"PurgeSoftDeletedOpportunitiesCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"PurgeSyncBatchesCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"RecalculateDealRisksCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"RemoveDeleteMarkersCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"RemoveExpiredNudgesCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"RemoveUnusedParticipantSpeechesCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"ResetElasticSearch.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"RestoreActivityCrmProviderIdCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"RestoreActivityTypeCommand.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"SeedActivities.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"SyncActivity.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"TrackImported.php, class","depth":10,"role_description":"text"}]...
|
7256175455732320930
|
8387909973189744798
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TrackAutomatedReportGeneratedEventTest
Run 'TrackAutomatedReportGeneratedEventTest'
Debug 'TrackAutomatedReportGeneratedEventTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
namespace Jiminny\Providers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
use Jiminny\Component\DealRisks\DealRisk;
use Jiminny\Exceptions\LogicException;
use Jiminny\Http\Controllers\CommentContextInterface;
use Jiminny\Http\Controllers\CustomerApi\CustomerApiController;
use Jiminny\Models;
use Jiminny\Models\Activity;
use Jiminny\Models\Ai\AiScorecard;
use Jiminny\Models\Ai\AiScorecardRule;
use Jiminny\Models\Ai\CrmTemplate;
use Jiminny\Models\Ai\CrmTemplateField;
use Jiminny\Models\CoachingFeedback;
use Jiminny\Models\CoachingSection;
use Jiminny\Models\Group;
use Jiminny\Models\Invitation;
use Jiminny\Models\JobTitle;
use Jiminny\Models\Nudge;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playbook;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Models\Playlist;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Models\Session;
use Jiminny\Models\Team;
use Jiminny\Models\Track;
use Jiminny\Models\User;
class RouteServiceProvider extends ServiceProvider
{
/**
* Define your route model bindings, pattern filters, etc.
*/
public function boot(): void
{
$this->configureRateLimiting();
$this->routes(function (Router $router) {
$this
->mapHealthRoutes($router)
->mapWebhookRoutes($router)
->mapApiRoutes($router)
->mapApiV2Routes($router)
->mapWebRoutes($router)
->mapScimRoutes($router)
->mapCustomerApiRoutes($router)
->mapEmbeddedRoutes($router)
;
});
$this->defineRouteBindings();
}
private function mapHealthRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
], static function (Router $router) {
require base_path('routes/health.php');
});
return $this;
}
/**
* Define the route model bindings.
*/
protected function defineRouteBindings()
{
Route::pattern('teamSlug', '^[.\-_a-z0-9]*$');
Route::pattern('userSlug', '^[.\-_a-z0-9]*$');
Route::bind('participant', function ($value) {
return Participant::uuid($value);
});
Route::bind('participantA', static fn (string $value): Participant => Participant::uuid($value));
Route::bind('participantB', static fn (string $value): Participant => Participant::uuid($value));
Route::bind('connection', function ($value) {
return Connection::uuid($value);
});
Route::bind('activity', function ($value) {
return Activity::uuid($value);
});
Route::bind('session', function (string $value) {
return Session::uuid($value);
});
Route::bind('transcription', function ($value) {
return Activity\Transcription::uuid($value);
});
Route::bind('transcriptionProvider', static function (string $scorecardUuid): Models\TranscriptionProvider {
return Models\TranscriptionProvider::where(
'uuid',
Models\TranscriptionProvider::toOptimized($scorecardUuid)
)->firstOrFail();
});
Route::bind('coachingFeedback', function ($value) {
return CoachingFeedback::uuid($value);
});
Route::bind('team', function ($value) {
return Team::uuid($value);
});
Route::bind('crmTemplate', fn ($value) => CrmTemplate::uuid($value));
Route::bind('crmTemplateField', fn ($value) => CrmTemplateField::uuid($value));
Route::bind('aiScorecard', fn ($value) => AiScorecard::uuid($value));
Route::bind('aiScorecardRule', fn ($value) => AiScorecardRule::uuid($value));
Route::bind('group', function ($value) {
return Group::uuid($value);
});
Route::bind('user', function ($value) {
return User::uuid($value);
});
Route::bind('comment', function (string $value, \Illuminate\Routing\Route $route) {
$targetController = $route->getController();
if (! $targetController instanceof CommentContextInterface) {
throw new LogicException(
'Type hinting comment will require additional effort due to polymorphism.'
. ' Either implement CommentContextInterface or inject $commentId and process manually',
);
}
$commentClass = $targetController::getCommentImplementation();
return $commentClass::uuid($value);
});
Route::bind('track', function ($value) {
return Track::uuid($value);
});
Route::bind('invitation', function ($value) {
return Invitation::uuid($value);
});
Route::bind('playbook', function ($value) {
return Playbook::uuid($value);
});
Route::bind('category', function ($value) {
return PlaybookCategory::uuid($value);
});
Route::bind('coachingSection', function ($value) {
return CoachingSection::uuid($value);
});
Route::bind('coachingSectionCriterion', function ($value) {
return Models\CoachingSectionCriterion::uuid($value);
});
Route::bind('playlist', function ($value) {
return Playlist::uuid($value);
});
Route::bind('playlistActivity', static fn (string $value) => PlaylistActivity::uuid($value));
Route::bind('playlistTrack', static fn (string $value) => PlaylistActivity::uuid($value));
Route::bind('playlistShare', static fn (string $uuid) => Playlist\Share::uuid($uuid));
Route::bind('job', function ($value) {
return JobTitle::uuid($value);
});
Route::bind('notification', function ($value) {
return Activity\AvailabilityNotification::uuid($value);
});
Route::bind('activityMoment', function ($value) {
return Activity\Moment::uuid($value);
});
Route::bind('search', function ($value) {
return Activity\Search::uuid($value);
});
Route::bind('nudge', static function ($value): Nudge {
return Nudge::uuid($value);
});
Route::bind('theme', static function ($value): Models\PlaybackTheme {
return Models\PlaybackTheme::uuid($value);
});
Route::bind('topic', static function ($value): Models\PlaybackTheme\Topic {
return Models\PlaybackTheme\Topic::uuid($value);
});
Route::bind('topicTrigger', static function ($value): Models\PlaybackTheme\TopicTrigger {
return Models\PlaybackTheme\TopicTrigger::uuid($value);
});
Route::bind('vocabulary', static function (string $id): Models\Vocabulary {
return Models\Vocabulary::uuid($id);
});
Route::bind('teamDomain', function ($value) {
return Models\TeamDomain::uuid($value);
});
Route::bind('opportunity', function ($value) {
return Opportunity::uuid($value);
});
Route::bind('dealRisk', function ($value) {
return DealRisk::uuid($value);
});
Route::bind('layout', static fn (string $uuid) => Models\Crm\Layout::uuid($uuid));
Route::bind('scorecard', static function (string $scorecardUuid): Models\Scorecard\Scorecard {
return Models\Scorecard\Scorecard::where(
'uuid',
Models\Scorecard\Scorecard::toOptimized($scorecardUuid)
)->firstOrFail();
});
Route::bind('scorecardRule', static function (string $scorecardRuleUuid): Models\Scorecard\ScorecardRule {
return Models\Scorecard\ScorecardRule::where(
'uuid',
Models\Scorecard\ScorecardRule::toOptimized($scorecardRuleUuid)
)->firstOrFail();
});
Route::bind('askAnythingPrompt', function ($value) {
return Models\AskAnything\AskAnythingPrompt::uuid($value);
});
}
/**
* Define the routes for the application.
*
* @param \Illuminate\Routing\Router $router
*/
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*/
private function mapWebRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['web'],
], static function (Router $router) {
require base_path('routes/web.php');
});
return $this;
}
/**
* Define the "Embedded" routes for the application.
*
* These routes depend on a Partitioned cookie,
* and are accessible only after login.
*/
private function mapEmbeddedRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['embedded'],
'prefix' => 'embedded',
], static function (Router $router) {
require base_path('routes/embedded.php');
});
return $this;
}
private function mapWebhookRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['webhook'],
'prefix' => 'webhook',
], function (Router $router) {
require base_path('routes/webhook.php');
});
return $this;
}
/**
* Define the "api" routes for the application.
*/
private function mapApiRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['api'],
'prefix' => 'api/v1',
], function (Router $router) {
require base_path('routes/api.php');
});
return $this;
}
private function mapApiV2Routes(Router $router): self
{
$router->group([
'middleware' => ['api', 'auth:api'],
'prefix' => 'api/v2',
], static function (Router $router) {
require base_path('routes/api_v2.php');
});
return $this;
}
private function mapScimRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['scim'],
'prefix' => 'scim/v2',
], function (Router $router) {
require base_path('routes/scim.php');
});
return $this;
}
private function mapCustomerApiRoutes(Router $router): self
{
$router->group([
'namespace' => $this->namespace,
'middleware' => ['customer-api'],
'prefix' => 'customer/api/v1',
], function (Router $router) {
require base_path('routes/customer_api.php');
});
return $this;
}
/** Configure the rate limiters for the application. */
protected function configureRateLimiting(): void
{
RateLimiter::for('api', static function (Request $request) {
$user = $request->user();
if ($user instanceof User || $user instanceof Models\Partner) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
if (! is_string($subject)) {
throw new LogicException('Unable to identify subject to rate limit');
}
return Limit::perMinute(120)->by($subject);
});
RateLimiter::for('customer-api', static function (Request $request) {
// Get the token using the same method as the middleware
$token = null;
if ($request->bearerToken() !== null) {
$token = $request->bearerToken();
} elseif ($request->get('api_token') !== null) {
$token = $request->get('api_token');
} elseif ($request->post('api_token') !== null) {
$token = $request->post('api_token');
}
// If we have a valid token, get the team ID for rate limiting
if ($token !== null && strlen($token) === Team::CUSTOMER_API_TOKEN_LENGTH) {
$tokens = Cache::remember(Team::CUSTOMER_API_TOKENS_CACHE, 60 * 60, static function () {
return Team::query()->whereNotNull('api_token')->pluck('id', 'api_token');
});
$hashedToken = hash('sha256', $token);
if (is_countable($tokens) && ! empty($tokens) && isset($tokens[$hashedToken])) {
$teamId = $tokens[$hashedToken];
$team = Team::find($teamId);
if ($team) {
$subject = $team->getUuid();
// Special rate limit for Funding Circle and Cision.
if (in_array($subject, [
'9762611a-fc86-4234-baba-a436de26e774', '1151b5b0-0680-4190-b30c-72649280837d',
'16b50903-a115-448f-877c-6c01e8b45f5d', 'bfc1c304-83b2-47a3-a7ce-1d28b0e1e90e',
'447ee473-7dd8-41bc-a702-9322e5b0ec5b'])) {
// Route-specific rate limiting
if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {
return Limit::perMinute(40)->by($subject);
}
return Limit::perMinute(1000)->by($subject);
}
// Route-specific rate limiting
if (Route::currentRouteAction() == CustomerApiController::class . '@getActivities') {
return Limit::perMinute(30)->by($subject);
}
return Limit::perMinute(120)->by($subject);
}
}
}
// Default fallback to IP-based limiting
return Limit::perMinute(120)->by($request->ip());
});
RateLimiter::for('conference-consent', static function (Request $request) {
// Rate limit by IP address for public consent endpoint
// Allow 10 requests per minute per IP to prevent abuse while allowing legitimate use
return Limit::perMinute(10)->by($request->ip());
});
RateLimiter::for('activity-export-shareable-link', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(30)->by($subject);
});
RateLimiter::for('activity-export', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(10)->by($subject);
});
RateLimiter::for('transcription-download', static function (Request $request) {
$user = $request->user();
if ($user instanceof User) {
$subject = $user->getUuid();
} else {
$subject = $request->ip();
}
return Limit::perMinute(3)->by($subject);
});
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
14
Previous Highlighted Error
Next Highlighted Error
{
"name": "jiminny/app",
"description": "The Jiminny Platform.",
"keywords": [
"training",
"salesforce",
"conference"
],
"license": "MIT",
"type": "project",
"require": {
"php": "^8.3",
"ext-ctype": "*",
"ext-curl": "*",
"ext-date": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-filter": "*",
"ext-gd": "*",
"ext-gmp": "*",
"ext-hash": "*",
"ext-iconv": "*",
"ext-igbinary": "*",
"ext-imagick": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mailparse": "*",
"ext-mbstring": "*",
"ext-mysqlnd": "*",
"ext-openssl": "*",
"ext-pcntl": "*",
"ext-pcre": "*",
"ext-pdo": "*",
"ext-pdo_mysql": "*",
"ext-phar": "*",
"ext-phpiredis": "*",
"ext-posix": "*",
"ext-readline": "*",
"ext-redis": "*",
"ext-reflection": "*",
"ext-session": "*",
"ext-simplexml": "*",
"ext-sockets": "*",
"ext-spl": "*",
"ext-tokenizer": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zend-opcache": "*",
"ext-zip": "*",
"ext-zlib": "*",
"lib-curl": "*",
"lib-curl-openssl": "*",
"lib-curl-zlib": "*",
"lib-date-timelib": "*",
"lib-date-zoneinfo": "*",
"lib-fileinfo-libmagic": "*",
"lib-gd": "*",
"lib-gd-freetype": "*",
"lib-gd-libjpeg": "*",
"lib-gd-libpng": "*",
"lib-gmp": "*",
"lib-icu": "*",
"lib-icu-cldr": "*",
"lib-icu-unicode": "*",
"lib-imagick-imagemagick": "*",
"lib-libxml": "*",
"lib-mbstring-libmbfl": "*",
"lib-mbstring-oniguruma": "*",
"lib-openssl": "*",
"lib-pcre": "*",
"lib-pcre-unicode": "*",
"lib-zip-libzip": "*",
"lib-zlib": "*",
"24slides/laravel-saml2": "^2.4",
"adam-paterson/oauth2-slack": "^1.1",
"asimlqt/php-google-spreadsheet-client": "^3.0",
"aws/aws-sdk-php": "^3.368",
"aws/aws-sdk-php-laravel": "^3.10",
"bepsvpt/secure-headers": "^9.0",
"chadhutchins/oauth2-slack": "^1.2",
"chaseconey/laravel-datadog-helper": "^1.2",
"chrisyue/php-m3u8": "4.0.3",
"daniti/oauth2-pipedrive": "dev-master",
"devio/pipedrive": "^2.6",
"doctrine/dbal": "^4.0",
"elasticsearch/elasticsearch": "^7.11",
"erusev/parsedown": "^1.7",
"fakerphp/faker": "^1.23",
"firebase/php-jwt": "^7.0",
"flipboxdigital/oauth2-hubspot": "1.0.1",
"giggsey/libphonenumber-for-php": "^8.12",
"google/apiclient": "^2.19",
"google/apiclient-services": "~0.360",
"google/apps-meet": "^0.5.1",
"guzzlehttp/guzzle": "^7.8",
"guzzlehttp/psr7": "^2.6",
"halaxa/json-machine": "^1.2",
"html2text/html2text": "^4.3",
"hubspot/api-client": "~5.0.0",
"hubspot/hubspot-php": "^5.2.0",
"intercom/intercom-php": "^4.5",
"intervention/image": "^3.4",
"jakeasmith/http_build_url": "^1.0",
"jdavidbakr/cloudfront-proxies": "^1.7",
"jeremykendall/php-domain-parser": "^6.3",
"jiminny/oauth2-aircall": "dev-master",
"jiminny/oauth2-bullhorn": "^0.2.0",
"jiminny/oauth2-dialpad": "dev-master",
"jiminny/oauth2-salesloft": "dev-master",
"jolicode/slack-php-api": "^4.5.0",
"kalnoy/nestedset": "*",
"laravel/framework": "^12.28",
"laravel/helpers": "^1.7",
"laravel/passport": "^13.0",
"laravel/slack-notification-channel": "^3.4",
"laravel/tinker": "^2.10.1",
"laravel/ui": "^4.6",
"laravolt/avatar": "^6.1",
"league/flysystem": "^3.0",
"league/flysystem-aws-s3-v3": "^3.0",
"league/fractal": "*",
"league/oauth2-client": "^2.7",
"league/oauth2-google": "^4.0",
"league/oauth2-linkedin": "^5.1",
"league/oauth2-server": "^9.2",
"league/statsd": "^2.0",
"markrogoyski/math-php": "^2.7.0",
"microsoft/microsoft-graph": "^2.51",
"monolog/monolog": "^3.0",
"nesbot/carbon": "^3.8",
"nette/caching": "*",
"phlib/sms-length": "^2.0",
"php-ffmpeg/php-ffmpeg": "^1.2",
"php-http/client-common": "^2.7",
"php-http/curl-client": "^2.3",
"php-http/httplug": "^2.2",
"php-http/message": "^1.16",
"phpseclib/phpseclib": "^3.0.36",
"propaganistas/laravel-phone": "^5.3",
"psr/cache": "^3.0",
"psr/http-message": "^2.0",
"psr/log": "^3.0",
"psr/simple-cache": "^3.0",
"pusher/pusher-php-server": "7.2.3",
"ramsey/uuid": "^4.2",
"ringcentral/ringcentral-php": "3.0.0",
"rmccue/requests": "^2.0",
"ruflin/elastica": "^7.1.1",
"santigarcor/laratrust": "^8.4",
"sentry/sentry": "4.13.0",
"sentry/sentry-laravel": "~4.13.0",
"shiftonelabs/laravel-sqs-fifo-queue": "^3.0",
"spatie/fractalistic": "^2.9",
"spatie/laravel-fractal": "^6.3",
"spatie/laravel-ignition": "^2.9",
"spatie/laravel-webhook-server": "^3.8",
"staudenmeir/belongs-to-through": "^2.17",
"stevenmaguire/oauth2-salesforce": "^2.0",
"symfony/cache": "^7.2",
"symfony/console": "^7.2",
"symfony/css-selector": "^7.2",
"symfony/debug": "^4.4",
"symfony/dom-crawler": "^7.2",
"symfony/expression-language": "^7.2",
"symfony/finder": "^7.2",
"symfony/http-client": "^7.3",
"symfony/http-foundation": "^7.2",
"symfony/http-kernel": "^7.2",
"symfony/postmark-mailer": "^7.3",
"symfony/process": "^7.3",
"symfony/property-access": "^7.2",
"symfony/psr-http-message-bridge": "^7.0",
"symfony/var-dumper": "^7.2",
"symfony/workflow": "^7.2",
"tecnickcom/tcpdf": "^6.11",
"thenetworg/oauth2-azure": "dev-master",
"tmannherz/oauth2-ringcentral": "dev-master",
"twilio/sdk": "^8.3",
"vanderlee/php-sentence": "^1.0",
"vinkla/hashids": "^13.0",
"vlucas/phpdotenv": "^5.4",
"wildbit/postmark-php": "^6.0",
"willdurand/email-reply-parser": "^2.8",
"zbateson/mail-mime-parser": "^3.0.4"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.15",
"barryvdh/laravel-ide-helper": "^3.5",
"brianium/paratest": "^7.5",
"browserstack/browserstack-local": "^1.1.0",
"filp/whoops": "^2.9",
"friendsofphp/php-cs-fixer": "^3.66",
"infection/infection": "^0.29.14",
"jasonmccreary/laravel-test-assertions": "^2.5",
"larastan/larastan": "^3.1",
"maglnet/composer-require-checker": "^4.8",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^11.5.50",
"symfony/phpunit-bridge": "^7.0",
"vimeo/psalm": "^6.5.0"
},
"autoload": {
"classmap": [
"database"
],
"psr-4": {
"Jiminny\\": "app/",
"Tests\\": "tests/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/",
"Microsoft\\Graph\\Generated\\Models\\": "app/Services/MeetingGenerator/Overrides/Microsoft/Graph/Generated/Models/"
},
"files": [
"app/helpers.php"
]
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
],
"psr-4": {
"Jiminny\\": "app/",
"Tests\\": "tests/"
}
},
"scripts": {
"post-root-package-install": [
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"php artisan key:generate --ansi"
],
"post-install-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postInstall"
],
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan ide-helper:generate",
"php artisan ide-helper:meta",
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
]
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true,
"allow-plugins": {
"infection/extension-installer": true,
"php-http/discovery": true,
"tbachert/spi": true
}
},
"extra": {
"laravel": {
"dont-discover": [
"laravel/dusk"
]
},
"metasyntactical/composer-plugin-license-check": {
"whitelist": [],
"blacklist": [
"AGPL"
]
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/PHP-FFMpeg/BinaryDriver.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-salesloft.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-aircall.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-pipedrive.git"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-ringcentral"
},
{
"type": "vcs",
"url": "https://github.com/jiminny/oauth2-dialpad.git"
}
],
"prefer-stable": true
}
Install
Update
Show log
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Configuration, folder
Console, folder
Commands, folder
Activities, folder
Analytics, folder
Calendars, folder
Crm, folder
DealInsights, folder
Dev, folder
Dialers, folder
DTOs, folder
Elasticsearch, folder
EngagementStats, folder
GeckoExport, folder
Livestream, folder
Mailboxes, folder
Migrate, folder
PlaybackThemes, folder
Playbooks, folder
Playlists, folder
Postmark, folder
ProphetAi, folder
Reports, folder
AutomatedReportsCommand.php, class
AutomatedReportsRetentionPolicyCommand.php, class
AutomatedReportsSendCommand.php, class
CreateMockAskJiminnyReportResultCommand.php, class
DeleteReportCommand.php, class
GenerateMarketingReport.php, class
Team.php, class
Usage.php, class
Slack, folder
Teams, folder
Tracks, folder
Transcription, folder
Twilio, folder
Users, folder
Vocabulary, folder
Zoom, folder
CoachingFeedbacksUpdateEsActivities.php, class
Command.php, class
CreateDatabaseUsers.php, class
DatabaseTableCount.php, class
DeleteOldAiCrmNotesCommand.php, class
DeleteS3LeftoversCommand.php, class
DevPostmanCommand.php, final class
DiarizeViaAiParticipantIdentificationCommand.php, class
EncryptTokensCommand.php, class
EngagementStatsRegenerateCommand.php, class
FeatureFlagsHelper.php
FixCrossTenantIssues.php, class
FlushRolesPermissionsCache.php, class
GenerateInternalWebhookToken.php, class
GroupSetDefaultLanguageCommand.php, final class
HelperTruncateCoachingTables.php, class
HubspotJournalPollingCommand.php, class
HubspotWebhookServiceCommand.php, class
ImportRecording.php, class
ImportUsersFromCsvFile.php, final class
IterateUsersCommand.php, abstract class
JiminnyCacheClearCommand.php, class
JiminnyDebugCommand.php, class
JiminnySetEncryptedTokenManagerModeCommand.php, class
JiminnyTokenInfoCommand.php, class
MakeSlackLiveCoachingChatNotesOn.php, class
ManageScimForTeam.php, class
MarkBranchForEnvironmentPipelineCommand.php, class
MuteOrganizerChannel.php, class
PhpApm.php, class
PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class
PurgeConferences.php, class
PurgeSoftDeletedOpportunitiesCommand.php, class
PurgeSyncBatchesCommand.php, class
RecalculateDealRisksCommand.php, class
RemoveDeleteMarkersCommand.php, class
RemoveExpiredNudgesCommand.php, class
RemoveUnusedParticipantSpeechesCommand.php, class
ResetElasticSearch.php, class
RestoreActivityCrmProviderIdCommand.php, class
RestoreActivityTypeCommand.php, class
SeedActivities.php, class
SyncActivity.php, class
TrackImported.php, class...
|
53772
|
|
52591
|
1139
|
2
|
2026-04-20T07:24:08.906115+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-20/1776 /Users/lukas/.screenpipe/data/data/2026-04-20/1776669848906_m2.jpg...
|
Firefox
|
fix(security): composer dependency updates – 2026- fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11970
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Checks pending
Checks pending
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Skipped alerts
Skipped alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#463
#463
phpunit/phpunit
high
—
12.5.22
releases
releases
Not safe:
Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple
TestCase
and
MockBuilder
methods (
iniSet()
,...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0018284575,"top":0.0518755,"width":0.07596409,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"bounds":{"left":0.0,"top":0.09497207,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.10614525,"width":0.09524601,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.12769353,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.13886672,"width":0.19963431,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.16041501,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.17158818,"width":0.15525267,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":4,"bounds":{"left":0.0,"top":0.19313647,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.20430966,"width":0.06981383,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":4,"bounds":{"left":0.0,"top":0.22585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.23703113,"width":0.10688165,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.2585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.2697526,"width":0.12915559,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.29130086,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.30247405,"width":0.014960106,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Product Growth Platform | Userpilot","depth":4,"bounds":{"left":0.0,"top":0.32402235,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Product Growth Platform | Userpilot","depth":5,"bounds":{"left":0.013297873,"top":0.33519554,"width":0.06200133,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Userpilot | Events","depth":4,"bounds":{"left":0.0,"top":0.3567438,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Userpilot | Events","depth":5,"bounds":{"left":0.013297873,"top":0.367917,"width":0.030418882,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.38946527,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.40063846,"width":0.2052859,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.39664805,"width":0.007978723,"height":0.01915403},"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.4237829,"width":0.07413564,"height":0.025538707},"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},"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},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Close bookmarks (⌘B)","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Bookmarks","depth":5,"bounds":{"left":0.083277926,"top":0.06943336,"width":0.026761968,"height":0.014764565},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bookmarks","depth":6,"bounds":{"left":0.083277926,"top":0.06943336,"width":0.026761968,"height":0.014764565},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close sidebar","depth":6,"bounds":{"left":0.1783577,"top":0.06424581,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXTextField","text":"Search bookmarks","depth":7,"bounds":{"left":0.082446806,"top":0.09976058,"width":0.107546546,"height":0.025538707},"help_text":"","role_description":"search text field","subrole":"AXSearchField","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (32)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"32","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (28)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"28","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 Edit title","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Checks pending","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks pending","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":16,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 23 additions & 23 deletions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (2)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (6)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"6","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (1)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":14,"bounds":{"left":0.40641624,"top":0.0726257,"width":0.011968086,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard","depth":14,"bounds":{"left":0.42503324,"top":0.058260176,"width":0.20428856,"height":0.042298485},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"fix(security): composer dependency updates – 2026-04-15","depth":16,"bounds":{"left":0.42503324,"top":0.05865922,"width":0.13397606,"height":0.01915403},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":17,"bounds":{"left":0.42503324,"top":0.06304868,"width":0.13397606,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":16,"bounds":{"left":0.5616689,"top":0.06304868,"width":0.0028257978,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":16,"bounds":{"left":0.56449467,"top":0.06304868,"width":0.012466756,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":18,"bounds":{"left":0.42503324,"top":0.08339984,"width":0.03873005,"height":0.011971269},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":19,"bounds":{"left":0.42503324,"top":0.08339984,"width":0.03873005,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":18,"bounds":{"left":0.46509308,"top":0.08339984,"width":0.057845745,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":18,"bounds":{"left":0.5242686,"top":0.08180367,"width":0.018450798,"height":0.015163607},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":19,"bounds":{"left":0.5262633,"top":0.083798885,"width":0.014461436,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":19,"bounds":{"left":0.5440492,"top":0.08339984,"width":0.00880984,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":19,"bounds":{"left":0.55418885,"top":0.08180367,"width":0.061502658,"height":0.015163607},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":20,"bounds":{"left":0.5561835,"top":0.083798885,"width":0.057513297,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":19,"bounds":{"left":0.61702126,"top":0.07821229,"width":0.00930851,"height":0.022346368},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Conversation","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@github-actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"github-actions bot commented 5 days ago •","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"5 days ago","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5 days ago","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Security dependency updates — composer — 2026-04-15","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Security dependency updates — composer — 2026-04-15","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"below.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CI run logs →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CI run logs →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Upgrade safety (changelog review)","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Upgrade safety (changelog review)","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall verdict:","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mixed","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(phpunit/phpunit) is listed under","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This does not replace CI, tests, or manual smoke checks before merge.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Fixed alerts","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Covered by bump to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#425).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Covered by bump to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#425).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Skipped alerts","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpunit/phpunit","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12.5.22","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Not safe:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TestCase","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"MockBuilder","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"methods (","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"iniSet()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
5785904048535128577
|
8384578334313033138
|
visual_change
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Checks pending
Checks pending
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Skipped alerts
Skipped alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#463
#463
phpunit/phpunit
high
—
12.5.22
releases
releases
Not safe:
Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple
TestCase
and
MockBuilder
methods (
iniSet()
,...
|
NULL
|
|
52598
|
1138
|
4
|
2026-04-20T07:24:33.565785+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-20/1776 /Users/lukas/.screenpipe/data/data/2026-04-20/1776669873565_m1.jpg...
|
Firefox
|
fix(security): composer dependency updates – 2026- fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11970
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Checks pending
Checks pending
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Skipped alerts
Skipped alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#463
#463
phpunit/phpunit
high
—
12.5.22
releases
releases
Not safe:
Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple
TestCase
and...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Product Growth Platform | Userpilot","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Product Growth Platform | Userpilot","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Userpilot | Events","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Userpilot | Events","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Close bookmarks (⌘B)","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Bookmarks","depth":5,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bookmarks","depth":6,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close sidebar","depth":6,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXTextField","text":"Search bookmarks","depth":7,"help_text":"","role_description":"search text field","subrole":"AXSearchField","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (32)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"32","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (28)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"28","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 Edit title","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Checks pending","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks pending","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":16,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 23 additions & 23 deletions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (2)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (6)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"6","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (1)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"fix(security): composer dependency updates – 2026-04-15","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":18,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":18,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":19,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":19,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Conversation","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@github-actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"github-actions bot commented 5 days ago •","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"5 days ago","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5 days ago","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Security dependency updates — composer — 2026-04-15","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Security dependency updates — composer — 2026-04-15","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"below.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CI run logs →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CI run logs →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Upgrade safety (changelog review)","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Upgrade safety (changelog review)","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall verdict:","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mixed","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(phpunit/phpunit) is listed under","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This does not replace CI, tests, or manual smoke checks before merge.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Fixed alerts","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Covered by bump to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#425).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Covered by bump to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#425).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Skipped alerts","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpunit/phpunit","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12.5.22","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Not safe:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TestCase","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-6346304660605159906
|
8384578334313033138
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Checks pending
Checks pending
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Skipped alerts
Skipped alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#463
#463
phpunit/phpunit
high
—
12.5.22
releases
releases
Not safe:
Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple
TestCase
and...
|
52596
|
|
52599
|
1139
|
7
|
2026-04-20T07:24:33.646156+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-20/1776 /Users/lukas/.screenpipe/data/data/2026-04-20/1776669873646_m2.jpg...
|
Firefox
|
fix(security): composer dependency updates – 2026- fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11970
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Checks pending
Checks pending
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Skipped alerts
Skipped alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#463
#463
phpunit/phpunit
high
—
12.5.22
releases
releases
Not safe:
Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple
TestCase
and
MockBuilder
methods (
iniSet()
,
setLocale()
,
getMockForAbstractClass()
,
getMockForTrait()
,
getObjectForTrait()
,
createTestProxy()
, etc.), drops support for doc-comment metadata (
@covers
,
@uses
annotations must be migrated to PHP 8 attributes), and removes several assert methods and CLI options. Breaking changes are fully documented — manual test-suite migration required before upgrading.
Alert
#463
#463
Package
phpunit/phpunit
Severity
high
CVE
—
Patched version
12.5.22...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0018284575,"top":0.0518755,"width":0.07596409,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"bounds":{"left":0.0,"top":0.09497207,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.10614525,"width":0.09524601,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.12769353,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.13886672,"width":0.19963431,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.16041501,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.17158818,"width":0.15525267,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":4,"bounds":{"left":0.0,"top":0.19313647,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.20430966,"width":0.06981383,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":4,"bounds":{"left":0.0,"top":0.22585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.23703113,"width":0.10688165,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.2585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.2697526,"width":0.12915559,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.29130086,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.30247405,"width":0.014960106,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Product Growth Platform | Userpilot","depth":4,"bounds":{"left":0.0,"top":0.32402235,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Product Growth Platform | Userpilot","depth":5,"bounds":{"left":0.013297873,"top":0.33519554,"width":0.06200133,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Userpilot | Events","depth":4,"bounds":{"left":0.0,"top":0.3567438,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Userpilot | Events","depth":5,"bounds":{"left":0.013297873,"top":0.367917,"width":0.030418882,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.38946527,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.40063846,"width":0.2052859,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.39664805,"width":0.007978723,"height":0.01915403},"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.4237829,"width":0.07413564,"height":0.025538707},"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},"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},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Close bookmarks (⌘B)","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Bookmarks","depth":5,"bounds":{"left":0.083277926,"top":0.06943336,"width":0.026761968,"height":0.014764565},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bookmarks","depth":6,"bounds":{"left":0.083277926,"top":0.06943336,"width":0.026761968,"height":0.014764565},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close sidebar","depth":6,"bounds":{"left":0.1783577,"top":0.06424581,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXTextField","text":"Search bookmarks","depth":7,"bounds":{"left":0.082446806,"top":0.09976058,"width":0.107546546,"height":0.025538707},"help_text":"","role_description":"search text field","subrole":"AXSearchField","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (32)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"32","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (28)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"28","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 Edit title","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Checks pending","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks pending","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":16,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 23 additions & 23 deletions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (2)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (6)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"6","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (1)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":14,"bounds":{"left":0.40641624,"top":0.0726257,"width":0.011968086,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard","depth":14,"bounds":{"left":0.42503324,"top":0.058260176,"width":0.20428856,"height":0.042298485},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"fix(security): composer dependency updates – 2026-04-15","depth":16,"bounds":{"left":0.42503324,"top":0.05865922,"width":0.13397606,"height":0.01915403},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":17,"bounds":{"left":0.42503324,"top":0.06304868,"width":0.13397606,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":16,"bounds":{"left":0.5616689,"top":0.06304868,"width":0.0028257978,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":16,"bounds":{"left":0.56449467,"top":0.06304868,"width":0.012466756,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":18,"bounds":{"left":0.42503324,"top":0.08339984,"width":0.03873005,"height":0.011971269},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":19,"bounds":{"left":0.42503324,"top":0.08339984,"width":0.03873005,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":18,"bounds":{"left":0.46509308,"top":0.08339984,"width":0.057845745,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":18,"bounds":{"left":0.5242686,"top":0.08180367,"width":0.018450798,"height":0.015163607},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":19,"bounds":{"left":0.5262633,"top":0.083798885,"width":0.014461436,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":19,"bounds":{"left":0.5440492,"top":0.08339984,"width":0.00880984,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":19,"bounds":{"left":0.55418885,"top":0.08180367,"width":0.061502658,"height":0.015163607},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":20,"bounds":{"left":0.5561835,"top":0.083798885,"width":0.057513297,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":19,"bounds":{"left":0.61702126,"top":0.07821229,"width":0.00930851,"height":0.022346368},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Conversation","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@github-actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"github-actions bot commented 5 days ago •","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"5 days ago","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5 days ago","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Security dependency updates — composer — 2026-04-15","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Security dependency updates — composer — 2026-04-15","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"below.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CI run logs →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CI run logs →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Upgrade safety (changelog review)","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Upgrade safety (changelog review)","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall verdict:","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mixed","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(phpunit/phpunit) is listed under","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This does not replace CI, tests, or manual smoke checks before merge.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Fixed alerts","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Covered by bump to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#425).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Covered by bump to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#425).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Skipped alerts","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpunit/phpunit","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12.5.22","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Not safe:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TestCase","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"MockBuilder","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"methods (","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"iniSet()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"setLocale()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getMockForAbstractClass()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getMockForTrait()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getObjectForTrait()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"createTestProxy()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", etc.), drops support for doc-comment metadata (","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"@covers","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"@uses","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"annotations must be migrated to PHP 8 attributes), and removes several assert methods and CLI options. Breaking changes are fully documented — manual test-suite migration required before upgrading.","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpunit/phpunit","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12.5.22","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-8701103347039174835
|
8384577784557219250
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Checks pending
Checks pending
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Skipped alerts
Skipped alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#463
#463
phpunit/phpunit
high
—
12.5.22
releases
releases
Not safe:
Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple
TestCase
and
MockBuilder
methods (
iniSet()
,
setLocale()
,
getMockForAbstractClass()
,
getMockForTrait()
,
getObjectForTrait()
,
createTestProxy()
, etc.), drops support for doc-comment metadata (
@covers
,
@uses
annotations must be migrated to PHP 8 attributes), and removes several assert methods and CLI options. Breaking changes are fully documented — manual test-suite migration required before upgrading.
Alert
#463
#463
Package
phpunit/phpunit
Severity
high
CVE
—
Patched version
12.5.22...
|
52597
|
|
52600
|
1138
|
5
|
2026-04-20T07:24:37.278965+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-20/1776 /Users/lukas/.screenpipe/data/data/2026-04-20/1776669877278_m1.jpg...
|
Firefox
|
fix(security): composer dependency updates – 2026- fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11970
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Checks pending
Checks pending
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Skipped alerts
Skipped alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#463
#463
phpunit/phpunit
high
—
12.5.22
releases
releases
Not safe:
Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple
TestCase
and
MockBuilder
methods (
iniSet()
,
setLocale()
,
getMockForAbstractClass()
,
getMockForTrait()
,
getObjectForTrait()
,
createTestProxy()
, etc.), drops support for doc-comment metadata (
@covers
,
@uses
annotations must be migrated to PHP 8 attributes), and removes several assert methods and CLI options. Breaking changes are fully documented — manual test-suite migration required before upgrading.
Alert
#463
#463...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Product Growth Platform | Userpilot","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Product Growth Platform | Userpilot","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Userpilot | Events","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Userpilot | Events","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Close bookmarks (⌘B)","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Bookmarks","depth":5,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bookmarks","depth":6,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close sidebar","depth":6,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXTextField","text":"Search bookmarks","depth":7,"help_text":"","role_description":"search text field","subrole":"AXSearchField","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (32)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"32","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (28)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"28","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 Edit title","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Checks pending","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks pending","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":16,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 23 additions & 23 deletions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (2)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (6)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"6","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (1)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"fix(security): composer dependency updates – 2026-04-15","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":18,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":18,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":19,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":19,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Conversation","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@github-actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"github-actions bot commented 5 days ago •","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"5 days ago","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5 days ago","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Security dependency updates — composer — 2026-04-15","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Security dependency updates — composer — 2026-04-15","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"below.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CI run logs →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CI run logs →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Upgrade safety (changelog review)","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Upgrade safety (changelog review)","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall verdict:","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mixed","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(phpunit/phpunit) is listed under","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This does not replace CI, tests, or manual smoke checks before merge.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Fixed alerts","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Covered by bump to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#425).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Covered by bump to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#425).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Skipped alerts","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpunit/phpunit","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12.5.22","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Not safe:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TestCase","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"MockBuilder","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"methods (","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"iniSet()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"setLocale()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getMockForAbstractClass()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getMockForTrait()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getObjectForTrait()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"createTestProxy()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", etc.), drops support for doc-comment metadata (","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"@covers","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"@uses","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"annotations must be migrated to PHP 8 attributes), and removes several assert methods and CLI options. Breaking changes are fully documented — manual test-suite migration required before upgrading.","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
8012986470236011409
|
8384577784557219250
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Checks pending
Checks pending
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Skipped alerts
Skipped alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#463
#463
phpunit/phpunit
high
—
12.5.22
releases
releases
Not safe:
Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple
TestCase
and
MockBuilder
methods (
iniSet()
,
setLocale()
,
getMockForAbstractClass()
,
getMockForTrait()
,
getObjectForTrait()
,
createTestProxy()
, etc.), drops support for doc-comment metadata (
@covers
,
@uses
annotations must be migrated to PHP 8 attributes), and removes several assert methods and CLI options. Breaking changes are fully documented — manual test-suite migration required before upgrading.
Alert
#463
#463...
|
NULL
|
|
52596
|
1138
|
3
|
2026-04-20T07:24:25.864093+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-20/1776 /Users/lukas/.screenpipe/data/data/2026-04-20/1776669865864_m1.jpg...
|
Firefox
|
fix(security): composer dependency updates – 2026- fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11970
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Checks pending
Checks pending
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Skipped alerts
Skipped alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#463
#463
phpunit/phpunit
high
—
12.5.22
releases
releases
Not safe:
Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple
TestCase
and
MockBuilder
methods (
iniSet()
,
setLocale()
,
getMockForAbstractClass()
,
getMockForTrait()
,
getObjectForTrait()
,
createTestProxy()
, etc.), drops support for doc-comment metadata (
@covers
,
@uses
annotations must be migrated to PHP 8 attributes), and removes several assert methods and CLI options. Breaking changes are fully documented — manual test-suite migration required before upgrading.
Alert
#463
#463
Package
phpunit/phpunit...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Product Growth Platform | Userpilot","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Product Growth Platform | Userpilot","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Userpilot | Events","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Userpilot | Events","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Close bookmarks (⌘B)","depth":6,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Bookmarks","depth":5,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bookmarks","depth":6,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close sidebar","depth":6,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXTextField","text":"Search bookmarks","depth":7,"help_text":"","role_description":"search text field","subrole":"AXSearchField","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (32)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"32","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (28)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"28","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 Edit title","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Checks pending","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks pending","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":16,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 23 additions & 23 deletions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (2)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (6)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"6","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (1)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"fix(security): composer dependency updates – 2026-04-15","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":18,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":18,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":19,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":19,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Conversation","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@github-actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"github-actions bot commented 5 days ago •","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"5 days ago","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5 days ago","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Security dependency updates — composer — 2026-04-15","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Security dependency updates — composer — 2026-04-15","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"below.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CI run logs →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CI run logs →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Upgrade safety (changelog review)","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Upgrade safety (changelog review)","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall verdict:","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mixed","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(phpunit/phpunit) is listed under","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This does not replace CI, tests, or manual smoke checks before merge.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Fixed alerts","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Covered by bump to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#425).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Covered by bump to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#425).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Skipped alerts","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpunit/phpunit","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12.5.22","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Not safe:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TestCase","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"MockBuilder","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"methods (","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"iniSet()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"setLocale()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getMockForAbstractClass()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getMockForTrait()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getObjectForTrait()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"createTestProxy()","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", etc.), drops support for doc-comment metadata (","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"@covers","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"@uses","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"annotations must be migrated to PHP 8 attributes), and removes several assert methods and CLI options. Breaking changes are fully documented — manual test-suite migration required before upgrading.","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpunit/phpunit","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
5616686192705113046
|
8383451884650376626
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Checks pending
Checks pending
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Skipped alerts
Skipped alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#463
#463
phpunit/phpunit
high
—
12.5.22
releases
releases
Not safe:
Major bump 11.x → 12.x. PHPUnit 12.0.0 removes multiple
TestCase
and
MockBuilder
methods (
iniSet()
,
setLocale()
,
getMockForAbstractClass()
,
getMockForTrait()
,
getObjectForTrait()
,
createTestProxy()
, etc.), drops support for doc-comment metadata (
@covers
,
@uses
annotations must be migrated to PHP 8 attributes), and removes several assert methods and CLI options. Breaking changes are fully documented — manual test-suite migration required before upgrading.
Alert
#463
#463
Package
phpunit/phpunit...
|
NULL
|
|
61480
|
1326
|
25
|
2026-04-21T06:59:52.945618+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-21/1776 /Users/lukas/.screenpipe/data/data/2026-04-21/1776754792945_m1.jpg...
|
Firefox
|
[SRD-6793] Les Mills activity types not pulling in [SRD-6793] Les Mills activity types not pulling in - Jira — Work...
|
1
|
jiminny.atlassian.net/jira/servicedesk/projects/SR jiminny.atlassian.net/jira/servicedesk/projects/SRD/queues/custom/37/SRD-6793...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Close tab
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny MCP Connector - Product - Confluence","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny MCP Connector - Product - Confluence","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny Mail","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny Mail","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-4417598424433628525
|
8380758852859819249
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Close tab
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira...
|
61479
|
|
60917
|
1312
|
29
|
2026-04-21T06:25:19.607341+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-21/1776 /Users/lukas/.screenpipe/data/data/2026-04-21/1776752719607_m1.jpg...
|
Firefox
|
JY-20701 | Reschedule HubSpot Sync Objects by yalo JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11989
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (
60 * 60 * 6
). Appropriate given the more frequent, presumably lighter runs.
Stagger delay
: HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.
Add or remove reactions
@yalokin-jiminny
yalokin-jiminny
yalokin-jiminny
force-pushed
force-pushed
the
JY-20553-debug-crm-sync-delays
branch from
f6c0ec3
f6c0ec3
to
5928f60
5928f60...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny MCP Connector - Product - Confluence","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny MCP Connector - Product - Confluence","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny Mail","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny Mail","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Feed — jiminny — Sentry","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Feed — jiminny — Sentry","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"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,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (31)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"31","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (21)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"21","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Review requested","depth":15,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Review requested","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requested your review on this pull request.","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add your review","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add your review","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11989","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Awaiting approval","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Awaiting approval","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 22 commits into","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701-reschedule-HubSpot-processing","depth":16,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701-reschedule-HubSpot-processing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 949 additions & 97 deletions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (5)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (22)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (11)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Conversation","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago •","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JIRA: JY-20701","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JIRA:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Casus:","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Casus:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"We want more fluent curve of the jobs distribution (no spikes) in","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm-sync","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"queue","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Changes:","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changes:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Remove HubSpot from SyncObjects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Create separate HubSpotSyncObjects that runs each 5 min instead of each 30","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Extract common code","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Improve processing Webhooks jobs distribution","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Optimize ImportOpportunityBatch:","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cache OpportunitySyncableFields, OwnerProfiles, RecordType","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"remove findContactByConfigurationAndId - Contact ID already exist","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"add timings log","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":16,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":13,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Author","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@claude","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"react with eyes","depth":14,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"👀","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":13,"bounds":{"left":0.14097223,"top":0.0,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"bounds":{"left":0.7125,"top":0.0,"width":0.016666668,"height":0.04111111},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"claude bot commented 4 days ago •","depth":13,"bounds":{"left":0.19166666,"top":0.0,"width":0.50416666,"height":0.041666668},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"claude","depth":15,"bounds":{"left":0.19166666,"top":0.0,"width":0.03125,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"claude","depth":16,"bounds":{"left":0.19166666,"top":0.0,"width":0.03125,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":16,"bounds":{"left":0.23020834,"top":0.0,"width":0.013541667,"height":0.016666668},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"bounds":{"left":0.25138888,"top":0.0,"width":0.053125,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"bounds":{"left":0.30729166,"top":0.0,"width":0.049305554,"height":0.023333333},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"bounds":{"left":0.30729166,"top":0.0,"width":0.049305554,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":16,"bounds":{"left":0.359375,"top":0.0,"width":0.004513889,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":16,"bounds":{"left":0.36666667,"top":0.0,"width":0.042013887,"height":0.023333333},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":18,"bounds":{"left":0.36666667,"top":0.0,"width":0.030902777,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Claude finished","depth":18,"bounds":{"left":0.19166666,"top":0.0,"width":0.07534722,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":18,"bounds":{"left":0.26701388,"top":0.0,"width":0.08020833,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@yalokin-jiminny","depth":19,"bounds":{"left":0.26701388,"top":0.0,"width":0.08020833,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s task in 1m 59s","depth":18,"bounds":{"left":0.3472222,"top":0.0,"width":0.07847222,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"——","depth":17,"bounds":{"left":0.42569444,"top":0.0,"width":0.022222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"View job","depth":17,"bounds":{"left":0.44791666,"top":0.0,"width":0.038194444,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"View job","depth":18,"bounds":{"left":0.44791666,"top":0.0,"width":0.038194444,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Code Review","depth":16,"bounds":{"left":0.19166666,"top":0.04388889,"width":0.5375,"height":0.02388889},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Code Review","depth":17,"bounds":{"left":0.19166666,"top":0.044444446,"width":0.07361111,"height":0.023333333},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall the approach is clean — extracting the shared logic into","depth":17,"bounds":{"left":0.19166666,"top":0.08777778,"width":0.28611112,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsCommandTrait","depth":18,"bounds":{"left":0.48125,"top":0.090555556,"width":0.11423611,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is a good call, and the scheduling design (offset cron at","depth":17,"bounds":{"left":0.19166666,"top":0.08777778,"width":0.5083333,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":01","depth":18,"bounds":{"left":0.3454861,"top":0.11388889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to avoid spikes) makes sense. I found two bugs and a few minor points.","depth":17,"bounds":{"left":0.3638889,"top":0.11111111,"width":0.32222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Bugs","depth":16,"bounds":{"left":0.19166666,"top":0.18944444,"width":0.5375,"height":0.024444444},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bugs","depth":17,"bounds":{"left":0.19166666,"top":0.19,"width":0.028819444,"height":0.023333333},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. Null dereference on","depth":18,"bounds":{"left":0.19166666,"top":0.2338889,"width":0.10451389,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig","depth":19,"bounds":{"left":0.29965279,"top":0.23666666,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"bounds":{"left":0.35277778,"top":0.2338889,"width":0.013541667,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:49-51","depth":19,"bounds":{"left":0.36979166,"top":0.23666666,"width":0.20416667,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.29222223,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.20763889,"top":0.29222223,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.25243056,"top":0.29222223,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.2673611,"top":0.29222223,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.27222222,"top":0.29222223,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.2923611,"top":0.29222223,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"bounds":{"left":0.30208334,"top":0.29222223,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.3170139,"top":0.29222223,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"bounds":{"left":0.20277777,"top":0.33055556,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.2125,"top":0.33055556,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.33055556,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.22743055,"top":0.33055556,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.27222222,"top":0.33055556,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"bounds":{"left":0.28229168,"top":0.33055556,"width":0.039930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"bounds":{"left":0.32222223,"top":0.33055556,"width":0.099305555,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"bounds":{"left":0.42152777,"top":0.33055556,"width":0.07986111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"bounds":{"left":0.5013889,"top":0.33055556,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.28166667,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"$team->crm","depth":18,"bounds":{"left":0.19479166,"top":0.3888889,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"can be null if the team has no CRM configuration record. Accessing","depth":17,"bounds":{"left":0.24791667,"top":0.3861111,"width":0.30729166,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->provider","depth":18,"bounds":{"left":0.55833334,"top":0.3888889,"width":0.05,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"on null will throw a fatal error. The existing","depth":17,"bounds":{"left":0.19166666,"top":0.3861111,"width":0.5277778,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":18,"bounds":{"left":0.27881944,"top":0.41222224,"width":0.05451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"job handles this by using","depth":17,"bounds":{"left":0.33680555,"top":0.40944445,"width":0.11666667,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"??","depth":18,"bounds":{"left":0.45694444,"top":0.41222224,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.47013888,"top":0.40944445,"width":0.00625,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig->provider ?? 'unknown'","depth":18,"bounds":{"left":0.4798611,"top":0.41222224,"width":0.16423611,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). The new job needs a null guard here:","depth":17,"bounds":{"left":0.19166666,"top":0.40944445,"width":0.51944447,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.49055555,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.20763889,"top":0.49055555,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.25243056,"top":0.49055555,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.2673611,"top":0.49055555,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.27222222,"top":0.49055555,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.2923611,"top":0.49055555,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"bounds":{"left":0.30208334,"top":0.49055555,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.3170139,"top":0.49055555,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"bounds":{"left":0.20277777,"top":0.5288889,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.2125,"top":0.5288889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.5288889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.22743055,"top":0.5288889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"===","depth":17,"bounds":{"left":0.27222222,"top":0.5288889,"width":0.025,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":17,"bounds":{"left":0.29722223,"top":0.5288889,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"||","depth":17,"bounds":{"left":0.3170139,"top":0.5288889,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.33715278,"top":0.5288889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.3420139,"top":0.5288889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.38680556,"top":0.5288889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"bounds":{"left":0.396875,"top":0.5288889,"width":0.039583333,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"bounds":{"left":0.43645832,"top":0.5288889,"width":0.099652775,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"bounds":{"left":0.5361111,"top":0.5288889,"width":0.07951389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"bounds":{"left":0.20277777,"top":0.5288889,"width":0.42777777,"height":0.035555556},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"logger","depth":17,"bounds":{"left":0.22743055,"top":0.54833335,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.25729167,"top":0.54833335,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"warning","depth":17,"bounds":{"left":0.2673611,"top":0.54833335,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.30208334,"top":0.54833335,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.30729166,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[SyncHubspotObjects] Team has no HubSpot CRM config","depth":17,"bounds":{"left":0.31215277,"top":0.54833335,"width":0.25381944,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.5659722,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", [","depth":17,"bounds":{"left":0.5708333,"top":0.54833335,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.5857639,"top":0.54833335,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.59097224,"top":0.54833335,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.6107639,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=>","depth":17,"bounds":{"left":0.615625,"top":0.54833335,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.6357639,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"bounds":{"left":0.640625,"top":0.54833335,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.66041666,"top":0.54833335,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"bounds":{"left":0.6704861,"top":0.54833335,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]);","depth":17,"bounds":{"left":0.20277777,"top":0.54833335,"width":0.5125,"height":0.035},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"return","depth":17,"bounds":{"left":0.22256945,"top":0.56722224,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";\n}","depth":17,"bounds":{"left":0.20277777,"top":0.56722224,"width":0.05451389,"height":0.035555556},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.48055556,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Fix this →","depth":17,"bounds":{"left":0.19166666,"top":0.6422222,"width":0.043055557,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"bounds":{"left":0.19166666,"top":0.6422222,"width":0.043055557,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.","depth":18,"bounds":{"left":0.19166666,"top":0.7227778,"width":0.011458334,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$lastSyncedAt","depth":19,"bounds":{"left":0.20659722,"top":0.72555554,"width":0.06458333,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is nullable but","depth":18,"bounds":{"left":0.27430555,"top":0.7227778,"width":0.07083333,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":19,"bounds":{"left":0.34861112,"top":0.72555554,"width":0.10451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requires","depth":18,"bounds":{"left":0.45625,"top":0.7227778,"width":0.04375,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":19,"bounds":{"left":0.5034722,"top":0.72555554,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"bounds":{"left":0.5364583,"top":0.7227778,"width":0.013888889,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:85,88,116","depth":19,"bounds":{"left":0.19166666,"top":0.72555554,"width":0.42673612,"height":0.039444443},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.8038889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.20763889,"top":0.8038889,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.2673611,"top":0.8038889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.28229168,"top":0.8038889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.28715277,"top":0.8038889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.33194444,"top":0.8038889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"last_synced_at","depth":17,"bounds":{"left":0.3420139,"top":0.8038889,"width":0.06979167,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.41180557,"top":0.8038889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// can be null on first run","depth":17,"bounds":{"left":0.42673612,"top":0.8038889,"width":0.134375,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.20277777,"top":0.8233333,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.20763889,"top":0.8233333,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.2125,"top":0.8233333,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"bounds":{"left":0.20763889,"top":0.8422222,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.22743055,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":17,"bounds":{"left":0.2375,"top":0.8422222,"width":0.10451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.3420139,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.346875,"top":0.8422222,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"bounds":{"left":0.35208333,"top":0.8422222,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"bounds":{"left":0.4017361,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.41180557,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.41666666,"top":0.8422222,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"bounds":{"left":0.4763889,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// ...","depth":17,"bounds":{"left":0.20277777,"top":0.88055557,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"private","depth":17,"bounds":{"left":0.20277777,"top":0.9,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"function","depth":17,"bounds":{"left":0.24236111,"top":0.9,"width":0.039930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects(","depth":17,"bounds":{"left":0.28229168,"top":0.9,"width":0.114583336,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"HubspotInterface","depth":17,"bounds":{"left":0.396875,"top":0.9,"width":0.07951389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.48125,"top":0.9,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"bounds":{"left":0.48645833,"top":0.9,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"bounds":{"left":0.5361111,"top":0.9,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":17,"bounds":{"left":0.54618055,"top":0.9,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.58090276,"top":0.9,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.5857639,"top":0.9,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"):","depth":17,"bounds":{"left":0.6454861,"top":0.9,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"void","depth":17,"bounds":{"left":0.66041666,"top":0.9,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.79388887,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"With","depth":17,"bounds":{"left":0.19166666,"top":0.95555556,"width":0.023263888,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"declare(strict_types=1)","depth":18,"bounds":{"left":0.21805556,"top":0.9583333,"width":0.114583336,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", passing","depth":17,"bounds":{"left":0.3361111,"top":0.95555556,"width":0.042708334,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":18,"bounds":{"left":0.38229167,"top":0.9583333,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to a","depth":17,"bounds":{"left":0.40555555,"top":0.95555556,"width":0.022222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":18,"bounds":{"left":0.43125,"top":0.9583333,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"-typed parameter throws a","depth":17,"bounds":{"left":0.4642361,"top":0.95555556,"width":0.12256944,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TypeError","depth":18,"bounds":{"left":0.5902778,"top":0.9583333,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". A team that has never synced before will crash on the first run. The parameter should be","depth":17,"bounds":{"left":0.19166666,"top":0.95555556,"width":0.5229167,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"?Carbon","depth":18,"bounds":{"left":0.52118057,"top":0.9816667,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(and","depth":17,"bounds":{"left":0.559375,"top":0.97888887,"width":0.025694445,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncOpportunities","depth":18,"bounds":{"left":0.5885417,"top":0.9816667,"width":0.084375,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"should handle null), or provide a fallback like","depth":17,"bounds":{"left":0.19166666,"top":0.97888887,"width":0.5173611,"height":0.02111113},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon::createFromTimestamp(0)","depth":18,"bounds":{"left":0.36423612,"top":1.0,"width":0.14930555,"height":-0.004999995},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.51666665,"top":1.0,"width":0.0027777778,"height":-0.0022221804},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Fix this →","depth":17,"bounds":{"left":0.19166666,"top":1.0,"width":0.043055557,"height":-0.043333292},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"bounds":{"left":0.19166666,"top":1.0,"width":0.043055557,"height":-0.043333292},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Minor Issues","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Minor Issues","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. Double call to","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix()","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"prefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() ?","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() .","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"''","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"getLogPrefix()","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is called twice. Trivial but unnecessary — capture the result in a variable first.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"command doesn't guard HubSpot when","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{team}","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"argument is given —","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/SyncObjects.php:40-41","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teams","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[] = Team::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"idOrUuId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// no provider check","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Running","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-objects <hubspot-team-id>","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dispatches a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsJob","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Design Notes","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Design Notes","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Scheduler placement","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is placed inside","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"scheduleEveryFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"but uses a manual cron","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"instead of","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->everyFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SQS visibility timeout","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncHubspotObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"uses 1 hour (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"60 * 60","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") vs","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s 6 hours (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"60 * 60 * 6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). Appropriate given the more frequent, presumably lighter runs.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Stagger delay","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"yalokin-jiminny","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"force-pushed","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"force-pushed","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"the","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20553-debug-crm-sync-delays","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"branch from","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"f6c0ec3","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"f6c0ec3","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"5928f60","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5928f60","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
3462865531066041227
|
8380486261257993549
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (
60 * 60 * 6
). Appropriate given the more frequent, presumably lighter runs.
Stagger delay
: HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.
Add or remove reactions
@yalokin-jiminny
yalokin-jiminny
yalokin-jiminny
force-pushed
force-pushed
the
JY-20553-debug-crm-sync-delays
branch from
f6c0ec3
f6c0ec3
to
5928f60
5928f60...
|
NULL
|
|
60925
|
1313
|
47
|
2026-04-21T06:25:41.861887+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-21/1776 /Users/lukas/.screenpipe/data/data/2026-04-21/1776752741861_m2.jpg...
|
Firefox
|
JY-20701 | Reschedule HubSpot Sync Objects by yalo JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11989
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (
60 * 60 * 6
). Appropriate given the more frequent, presumably lighter runs.
Stagger delay
: HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.
Add or remove reactions
@yalokin-jiminny
yalokin-jiminny
yalokin-jiminny
force-pushed
force-pushed
the
JY-20553-debug-crm-sync-delays
branch from
f6c0ec3
f6c0ec3
to
5928f60
5928f60
Compare...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0018284575,"top":0.0518755,"width":0.07596409,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"bounds":{"left":0.0,"top":0.09497207,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.10614525,"width":0.08344415,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"bounds":{"left":0.0,"top":0.12769353,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.13886672,"width":0.08344415,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.16041501,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.17158818,"width":0.19963431,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny MCP Connector - Product - Confluence","depth":4,"bounds":{"left":0.0,"top":0.19313647,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny MCP Connector - Product - Confluence","depth":5,"bounds":{"left":0.013297873,"top":0.20430966,"width":0.08294548,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":4,"bounds":{"left":0.0,"top":0.22585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.23703113,"width":0.15791224,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny Mail","depth":4,"bounds":{"left":0.0,"top":0.2585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny Mail","depth":5,"bounds":{"left":0.013297873,"top":0.2697526,"width":0.02144282,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":4,"bounds":{"left":0.0,"top":0.29130086,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.30247405,"width":0.08610372,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Feed — jiminny — Sentry","depth":4,"bounds":{"left":0.0,"top":0.32402235,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Feed — jiminny — Sentry","depth":5,"bounds":{"left":0.013297873,"top":0.33519554,"width":0.042719416,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.0,"top":0.3567438,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.013297873,"top":0.367917,"width":0.013131649,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.38946527,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.40063846,"width":0.1740359,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.39664805,"width":0.007978723,"height":0.01915403},"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.4237829,"width":0.07413564,"height":0.025538707},"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},"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},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"bounds":{"left":0.07962101,"top":0.0518755,"width":0.0003324468,"height":0.0007980846},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"bounds":{"left":0.07962101,"top":0.05347167,"width":0.0029920214,"height":0.21468475},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"bounds":{"left":0.08494016,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"bounds":{"left":0.099567816,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"bounds":{"left":0.112865694,"top":0.06464485,"width":0.018949468,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"bounds":{"left":0.11486037,"top":0.07063048,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"bounds":{"left":0.13680187,"top":0.06464485,"width":0.017785905,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"bounds":{"left":0.13879654,"top":0.07063048,"width":0.008477394,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"bounds":{"left":0.81698805,"top":0.06464485,"width":0.06565824,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"bounds":{"left":0.82928854,"top":0.07063048,"width":0.011801862,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"bounds":{"left":0.8424202,"top":0.07222666,"width":0.002493351,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"bounds":{"left":0.84640956,"top":0.07063048,"width":0.021276595,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"bounds":{"left":0.88464093,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"bounds":{"left":0.8949468,"top":0.06464485,"width":0.008643617,"height":0.025538707},"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"bounds":{"left":0.9115692,"top":0.06464485,"width":0.01662234,"height":0.025538707},"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"bounds":{"left":0.93085104,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"bounds":{"left":0.94414896,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"bounds":{"left":0.9574468,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"bounds":{"left":0.97074467,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"bounds":{"left":0.9840425,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"bounds":{"left":0.079288565,"top":0.051077414,"width":0.0003324468,"height":0.0007980846},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"bounds":{"left":0.079288565,"top":0.05387071,"width":0.0787899,"height":0.023144454},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"bounds":{"left":0.08494016,"top":0.09936153,"width":0.025099734,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"bounds":{"left":0.095744684,"top":0.10574621,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (31)","depth":12,"bounds":{"left":0.11269947,"top":0.09936153,"width":0.054521278,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"bounds":{"left":0.12333777,"top":0.10574621,"width":0.02925532,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"bounds":{"left":0.15525267,"top":0.113727055,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"31","depth":14,"bounds":{"left":0.15824468,"top":0.113727055,"width":0.004986702,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"bounds":{"left":0.16323139,"top":0.113727055,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"bounds":{"left":0.16988032,"top":0.09936153,"width":0.029089095,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"bounds":{"left":0.18085106,"top":0.10574621,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"bounds":{"left":0.20162898,"top":0.09936153,"width":0.03025266,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"bounds":{"left":0.21276596,"top":0.10574621,"width":0.015957447,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"bounds":{"left":0.23454122,"top":0.09936153,"width":0.022938829,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"bounds":{"left":0.24534574,"top":0.10574621,"width":0.009142287,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (21)","depth":12,"bounds":{"left":0.2601396,"top":0.09936153,"width":0.069980055,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"bounds":{"left":0.27194148,"top":0.10574621,"width":0.04255319,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"bounds":{"left":0.31831783,"top":0.113727055,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"21","depth":14,"bounds":{"left":0.32130983,"top":0.113727055,"width":0.0048204786,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"bounds":{"left":0.32613033,"top":0.113727055,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"bounds":{"left":0.33277926,"top":0.09936153,"width":0.03125,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"bounds":{"left":0.34391624,"top":0.10574621,"width":0.016788565,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"bounds":{"left":0.36668882,"top":0.09936153,"width":0.032081116,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"bounds":{"left":0.3778258,"top":0.10574621,"width":0.01761968,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"bounds":{"left":0.09325133,"top":0.14365523,"width":0.0003324468,"height":0.016759777},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"bounds":{"left":0.09325133,"top":0.1452514,"width":0.039228722,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"bounds":{"left":0.09325133,"top":0.1452514,"width":0.2159242,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"bounds":{"left":0.30917552,"top":0.1452514,"width":0.04055851,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"bounds":{"left":0.30917552,"top":0.1452514,"width":0.04055851,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"bounds":{"left":0.34973404,"top":0.1452514,"width":0.08261303,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"bounds":{"left":0.4323471,"top":0.1452514,"width":0.05219415,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"bounds":{"left":0.4323471,"top":0.1452514,"width":0.05219415,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"bounds":{"left":0.48454124,"top":0.1452514,"width":0.0013297872,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"bounds":{"left":0.98636967,"top":0.13886672,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Review requested","depth":15,"bounds":{"left":0.35006648,"top":0.2019154,"width":0.0003324468,"height":0.016759777},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Review requested","depth":16,"bounds":{"left":0.35006648,"top":0.20351157,"width":0.039893616,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"bounds":{"left":0.35006648,"top":0.20351157,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"bounds":{"left":0.35006648,"top":0.20351157,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requested your review on this pull request.","depth":15,"bounds":{"left":0.3854721,"top":0.20351157,"width":0.09158909,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add your review","depth":14,"bounds":{"left":0.70212764,"top":0.19872306,"width":0.036901597,"height":0.022346368},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add your review","depth":16,"bounds":{"left":0.70511967,"top":0.20391062,"width":0.030917553,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title","depth":13,"bounds":{"left":0.33776596,"top":0.24221867,"width":0.26230052,"height":0.031923383},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects","depth":14,"bounds":{"left":0.33776596,"top":0.24301676,"width":0.2122673,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"bounds":{"left":0.55269283,"top":0.24301676,"width":0.006482713,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11989","depth":15,"bounds":{"left":0.55917555,"top":0.24301676,"width":0.028922873,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"bounds":{"left":0.5894282,"top":0.24541101,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Awaiting approval","depth":13,"bounds":{"left":0.6569149,"top":0.24860336,"width":0.055518616,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Awaiting approval","depth":15,"bounds":{"left":0.66921544,"top":0.254589,"width":0.038896278,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"bounds":{"left":0.7137633,"top":0.24860336,"width":0.02825798,"height":0.025538707},"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"bounds":{"left":0.7180851,"top":0.254589,"width":0.011635638,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"bounds":{"left":0.34840426,"top":0.28651237,"width":0.011968086,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"bounds":{"left":0.36702126,"top":0.28332004,"width":0.034075797,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"bounds":{"left":0.36702126,"top":0.2849162,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 22 commits into","depth":15,"bounds":{"left":0.40242687,"top":0.2849162,"width":0.069148935,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"bounds":{"left":0.47290558,"top":0.282921,"width":0.018284574,"height":0.017557861},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"bounds":{"left":0.47490028,"top":0.28611332,"width":0.014295213,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"bounds":{"left":0.49251994,"top":0.2849162,"width":0.009973404,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701-reschedule-HubSpot-processing","depth":16,"bounds":{"left":0.50382316,"top":0.282921,"width":0.09524601,"height":0.017557861},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701-reschedule-HubSpot-processing","depth":17,"bounds":{"left":0.50581783,"top":0.28611332,"width":0.09125665,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"bounds":{"left":0.60039896,"top":0.28052673,"width":0.00930851,"height":0.022346368},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 949 additions & 97 deletions","depth":14,"bounds":{"left":0.7067819,"top":0.3367917,"width":0.019946808,"height":0.11412609},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (5)","depth":16,"bounds":{"left":0.33776596,"top":0.31883478,"width":0.057347074,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"bounds":{"left":0.35139626,"top":0.32841182,"width":0.028091755,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.38946143,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5","depth":18,"bounds":{"left":0.39245346,"top":0.32841182,"width":0.0028257978,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.39527926,"top":0.32841182,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (22)","depth":16,"bounds":{"left":0.39511302,"top":0.31883478,"width":0.05069814,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"bounds":{"left":0.40874335,"top":0.32841182,"width":0.019115692,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.4401596,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22","depth":18,"bounds":{"left":0.4431516,"top":0.32841182,"width":0.005485372,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.44863698,"top":0.32841182,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (3)","depth":16,"bounds":{"left":0.44581118,"top":0.31883478,"width":0.04504654,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"bounds":{"left":0.45944148,"top":0.32841182,"width":0.015957447,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.48520613,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"bounds":{"left":0.48819813,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.49119017,"top":0.32841182,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (11)","depth":16,"bounds":{"left":0.49085772,"top":0.31883478,"width":0.060339097,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"bounds":{"left":0.50448805,"top":0.32841182,"width":0.029753989,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.5455452,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11","depth":18,"bounds":{"left":0.54853725,"top":0.32841182,"width":0.0043218085,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.55285907,"top":0.32841182,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Conversation","depth":12,"bounds":{"left":0.33776596,"top":0.3643256,"width":0.0003324468,"height":0.0007980846},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"bounds":{"left":0.33776596,"top":0.36711892,"width":0.048204787,"height":0.023144454},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":12,"bounds":{"left":0.33776596,"top":0.3643256,"width":0.013297873,"height":0.031923383},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"bounds":{"left":0.61136967,"top":0.3651237,"width":0.007978723,"height":0.02952913},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago •","depth":14,"bounds":{"left":0.3620346,"top":0.3651237,"width":0.24135639,"height":0.02952913},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":16,"bounds":{"left":0.3620346,"top":0.37310454,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":17,"bounds":{"left":0.3620346,"top":0.37310454,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"bounds":{"left":0.39744017,"top":0.37310454,"width":0.025598405,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":15,"bounds":{"left":0.42436835,"top":0.3715084,"width":0.023603724,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":17,"bounds":{"left":0.42436835,"top":0.37310454,"width":0.023603724,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"bounds":{"left":0.44930187,"top":0.37310454,"width":0.0021609042,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"bounds":{"left":0.45262632,"top":0.3715084,"width":0.020279255,"height":0.016759777},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"bounds":{"left":0.45262632,"top":0.37310454,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JIRA: JY-20701","depth":16,"bounds":{"left":0.3620346,"top":0.40822026,"width":0.25731382,"height":0.017557861},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JIRA:","depth":17,"bounds":{"left":0.3620346,"top":0.4086193,"width":0.015791224,"height":0.016759777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701","depth":17,"bounds":{"left":0.3778258,"top":0.4086193,"width":0.026263298,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701","depth":18,"bounds":{"left":0.3778258,"top":0.4086193,"width":0.026263298,"height":0.016759777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Casus:","depth":16,"bounds":{"left":0.3620346,"top":0.44493216,"width":0.25731382,"height":0.01396648},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Casus:","depth":17,"bounds":{"left":0.3620346,"top":0.44493216,"width":0.015292553,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"We want more fluent curve of the jobs distribution (no spikes) in","depth":18,"bounds":{"left":0.3700133,"top":0.47326416,"width":0.13879654,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm-sync","depth":19,"bounds":{"left":0.51047206,"top":0.47525936,"width":0.018949468,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"queue","depth":18,"bounds":{"left":0.53108376,"top":0.47326416,"width":0.01462766,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times","depth":18,"bounds":{"left":0.3700133,"top":0.49281725,"width":0.24750665,"height":0.030726258},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Changes:","depth":16,"bounds":{"left":0.3620346,"top":0.5442937,"width":0.25731382,"height":0.01396648},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changes:","depth":17,"bounds":{"left":0.3620346,"top":0.5442937,"width":0.021110373,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Remove HubSpot from SyncObjects","depth":18,"bounds":{"left":0.3700133,"top":0.5726257,"width":0.07712766,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Create separate HubSpotSyncObjects that runs each 5 min instead of each 30","depth":18,"bounds":{"left":0.3700133,"top":0.59217876,"width":0.16855054,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Extract common code","depth":18,"bounds":{"left":0.3700133,"top":0.6121309,"width":0.047041222,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Improve processing Webhooks jobs distribution","depth":18,"bounds":{"left":0.3700133,"top":0.63168395,"width":0.101894945,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Optimize ImportOpportunityBatch:","depth":18,"bounds":{"left":0.3700133,"top":0.65163606,"width":0.07396942,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cache OpportunitySyncableFields, OwnerProfiles, RecordType","depth":20,"bounds":{"left":0.37799203,"top":0.6683959,"width":0.13347739,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"remove findContactByConfigurationAndId - Contact ID already exist","depth":20,"bounds":{"left":0.37799203,"top":0.68834794,"width":0.1456117,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"add timings log","depth":20,"bounds":{"left":0.37799203,"top":0.70790106,"width":0.032912236,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":16,"bounds":{"left":0.3620346,"top":0.73623306,"width":0.008643617,"height":0.0207502},"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":13,"bounds":{"left":0.33776596,"top":0.7960894,"width":0.013297873,"height":0.031923383},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"bounds":{"left":0.61136967,"top":0.79688746,"width":0.007978723,"height":0.02952913},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Author","depth":15,"bounds":{"left":0.5934175,"top":0.80606544,"width":0.012965426,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago","depth":13,"bounds":{"left":0.3620346,"top":0.79688746,"width":0.22240691,"height":0.02952913},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"bounds":{"left":0.3620346,"top":0.80486834,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"bounds":{"left":0.3620346,"top":0.80486834,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"bounds":{"left":0.39744017,"top":0.80486834,"width":0.025598405,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"bounds":{"left":0.42436835,"top":0.8032721,"width":0.023603724,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"bounds":{"left":0.42436835,"top":0.80486834,"width":0.023603724,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":17,"bounds":{"left":0.3620346,"top":0.8415802,"width":0.019115692,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@claude","depth":18,"bounds":{"left":0.3620346,"top":0.8415802,"width":0.019115692,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"bounds":{"left":0.3620346,"top":0.86951315,"width":0.008643617,"height":0.0207502},"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"react with eyes","depth":14,"bounds":{"left":0.37200797,"top":0.86951315,"width":0.013796543,"height":0.0207502},"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"👀","depth":16,"bounds":{"left":0.37416887,"top":0.8747007,"width":0.004155585,"height":0.012769354},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":16,"bounds":{"left":0.38098404,"top":0.8747007,"width":0.0018284575,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":13,"bounds":{"left":0.33776596,"top":0.9293695,"width":0.013297873,"height":0.031923383},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"bounds":{"left":0.61136967,"top":0.9301676,"width":0.007978723,"height":0.02952913},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"claude bot commented 4 days ago •","depth":13,"bounds":{"left":0.3620346,"top":0.9301676,"width":0.24135639,"height":0.029928172},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"claude","depth":15,"bounds":{"left":0.3620346,"top":0.93814844,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"claude","depth":16,"bounds":{"left":0.3620346,"top":0.93814844,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":16,"bounds":{"left":0.3804854,"top":0.9397446,"width":0.006482713,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"bounds":{"left":0.390625,"top":0.93814844,"width":0.02543218,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"bounds":{"left":0.41738698,"top":0.9365523,"width":0.023603724,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"bounds":{"left":0.41738698,"top":0.93814844,"width":0.023603724,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":16,"bounds":{"left":0.44232047,"top":0.93814844,"width":0.0021609042,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":16,"bounds":{"left":0.44581118,"top":0.9365523,"width":0.020113032,"height":0.016759777},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":18,"bounds":{"left":0.44581118,"top":0.93814844,"width":0.014793883,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Claude finished","depth":18,"bounds":{"left":0.3620346,"top":0.97525936,"width":0.036070477,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":18,"bounds":{"left":0.39810506,"top":0.97525936,"width":0.038397606,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@yalokin-jiminny","depth":19,"bounds":{"left":0.39810506,"top":0.97525936,"width":0.038397606,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s task in 1m 59s","depth":18,"bounds":{"left":0.43650267,"top":0.97525936,"width":0.03756649,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"——","depth":17,"bounds":{"left":0.47406915,"top":0.97525936,"width":0.010638298,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"View job","depth":17,"bounds":{"left":0.48470744,"top":0.97525936,"width":0.018284574,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"View job","depth":18,"bounds":{"left":0.48470744,"top":0.97525936,"width":0.018284574,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Code Review","depth":16,"bounds":{"left":0.3620346,"top":1.0,"width":0.25731382,"height":-0.0315243},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Code Review","depth":17,"bounds":{"left":0.3620346,"top":1.0,"width":0.03523936,"height":-0.031923413},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall the approach is clean — extracting the shared logic into","depth":17,"bounds":{"left":0.3620346,"top":1.0,"width":0.13696809,"height":-0.06304872},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsCommandTrait","depth":18,"bounds":{"left":0.5006649,"top":1.0,"width":0.0546875,"height":-0.065043926},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is a good call, and the scheduling design (offset cron at","depth":17,"bounds":{"left":0.3620346,"top":1.0,"width":0.24335106,"height":-0.06304872},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":01","depth":18,"bounds":{"left":0.43567154,"top":1.0,"width":0.0071476065,"height":-0.08180368},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to avoid spikes) makes sense. I found two bugs and a few minor points.","depth":17,"bounds":{"left":0.44448137,"top":1.0,"width":0.15425532,"height":-0.07980847},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Bugs","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bugs","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. Null dereference on","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:49-51","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"$team->crm","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"can be null if the team has no CRM configuration record. Accessing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->provider","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"on null will throw a fatal error. The existing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"job handles this by using","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"??","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig->provider ?? 'unknown'","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). The new job needs a null guard here:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"===","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"||","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"logger","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"warning","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[SyncHubspotObjects] Team has no HubSpot CRM config","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", [","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=>","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]);","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"return","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";\n}","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Fix this →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$lastSyncedAt","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is nullable but","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requires","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:85,88,116","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"last_synced_at","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// can be null on first run","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// ...","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"private","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"function","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"HubspotInterface","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"):","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"void","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"With","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"declare(strict_types=1)","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", passing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"-typed parameter throws a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TypeError","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". A team that has never synced before will crash on the first run. The parameter should be","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"?Carbon","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(and","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncOpportunities","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"should handle null), or provide a fallback like","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon::createFromTimestamp(0)","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Fix this →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Minor Issues","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Minor Issues","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. Double call to","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix()","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"prefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() ?","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() .","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"''","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"getLogPrefix()","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is called twice. Trivial but unnecessary — capture the result in a variable first.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"command doesn't guard HubSpot when","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{team}","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"argument is given —","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/SyncObjects.php:40-41","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teams","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[] = Team::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"idOrUuId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// no provider check","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Running","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-objects <hubspot-team-id>","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dispatches a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsJob","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Design Notes","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Design Notes","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Scheduler placement","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is placed inside","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"scheduleEveryFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"but uses a manual cron","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"instead of","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->everyFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SQS visibility timeout","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncHubspotObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"uses 1 hour (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"60 * 60","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") vs","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s 6 hours (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"60 * 60 * 6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). Appropriate given the more frequent, presumably lighter runs.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Stagger delay","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"yalokin-jiminny","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"force-pushed","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"force-pushed","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"the","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20553-debug-crm-sync-delays","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"branch from","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"f6c0ec3","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"f6c0ec3","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"5928f60","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5928f60","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Compare","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-7546802111406910005
|
8380486261257993549
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (
60 * 60 * 6
). Appropriate given the more frequent, presumably lighter runs.
Stagger delay
: HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.
Add or remove reactions
@yalokin-jiminny
yalokin-jiminny
yalokin-jiminny
force-pushed
force-pushed
the
JY-20553-debug-crm-sync-delays
branch from
f6c0ec3
f6c0ec3
to
5928f60
5928f60
Compare...
|
60923
|
|
60918
|
1313
|
42
|
2026-04-21T06:25:19.665338+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-21/1776 /Users/lukas/.screenpipe/data/data/2026-04-21/1776752719665_m2.jpg...
|
Firefox
|
JY-20701 | Reschedule HubSpot Sync Objects by yalo JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11989
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (
60 * 60 * 6
). Appropriate given the more frequent, presumably lighter runs.
Stagger delay
: HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.
Add or remove reactions
@yalokin-jiminny
yalokin-jiminny
yalokin-jiminny
force-pushed
force-pushed
the
JY-20553-debug-crm-sync-delays
branch from
f6c0ec3
f6c0ec3
to
5928f60
5928f60
Compare
Compare
yesterday
yesterday
@yalokin-jiminny
yalokin-jiminny
yalokin-jiminny
force-pushed
force-pushed
the
JY-20701-reschedule-HubSpot-processing
branch from
3d7234a
3d7234a
to
1d10796
1d10796
Compare
Compare
yesterday
yesterday
Base automatically changed from
JY-20553-debug-crm-sync-delays
to
master
19 hours ago...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0018284575,"top":0.0518755,"width":0.07596409,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"bounds":{"left":0.0,"top":0.09497207,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.10614525,"width":0.08344415,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"bounds":{"left":0.0,"top":0.12769353,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.13886672,"width":0.08344415,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.16041501,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.17158818,"width":0.19963431,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny MCP Connector - Product - Confluence","depth":4,"bounds":{"left":0.0,"top":0.19313647,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny MCP Connector - Product - Confluence","depth":5,"bounds":{"left":0.013297873,"top":0.20430966,"width":0.08294548,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":4,"bounds":{"left":0.0,"top":0.22585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.23703113,"width":0.15791224,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny Mail","depth":4,"bounds":{"left":0.0,"top":0.2585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny Mail","depth":5,"bounds":{"left":0.013297873,"top":0.2697526,"width":0.02144282,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":4,"bounds":{"left":0.0,"top":0.29130086,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.30247405,"width":0.08610372,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Feed — jiminny — Sentry","depth":4,"bounds":{"left":0.0,"top":0.32402235,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Feed — jiminny — Sentry","depth":5,"bounds":{"left":0.013297873,"top":0.33519554,"width":0.042719416,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.0,"top":0.3567438,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.013297873,"top":0.367917,"width":0.013131649,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.38946527,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.40063846,"width":0.1740359,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.39664805,"width":0.007978723,"height":0.01915403},"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.4237829,"width":0.07413564,"height":0.025538707},"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},"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},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"bounds":{"left":0.07962101,"top":0.0518755,"width":0.0003324468,"height":0.0007980846},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"bounds":{"left":0.07962101,"top":0.05347167,"width":0.0029920214,"height":0.21468475},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"bounds":{"left":0.08494016,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"bounds":{"left":0.099567816,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"bounds":{"left":0.112865694,"top":0.06464485,"width":0.018949468,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"bounds":{"left":0.11486037,"top":0.07063048,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"bounds":{"left":0.13680187,"top":0.06464485,"width":0.017785905,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"bounds":{"left":0.13879654,"top":0.07063048,"width":0.008477394,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"bounds":{"left":0.81698805,"top":0.06464485,"width":0.06565824,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"bounds":{"left":0.82928854,"top":0.07063048,"width":0.011801862,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"bounds":{"left":0.8424202,"top":0.07222666,"width":0.002493351,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"bounds":{"left":0.84640956,"top":0.07063048,"width":0.021276595,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"bounds":{"left":0.88464093,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"bounds":{"left":0.8949468,"top":0.06464485,"width":0.008643617,"height":0.025538707},"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"bounds":{"left":0.9115692,"top":0.06464485,"width":0.01662234,"height":0.025538707},"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"bounds":{"left":0.93085104,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"bounds":{"left":0.94414896,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"bounds":{"left":0.9574468,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"bounds":{"left":0.97074467,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"bounds":{"left":0.9840425,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"bounds":{"left":0.079288565,"top":0.051077414,"width":0.0003324468,"height":0.0007980846},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"bounds":{"left":0.079288565,"top":0.05387071,"width":0.0787899,"height":0.023144454},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"bounds":{"left":0.08494016,"top":0.09936153,"width":0.025099734,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"bounds":{"left":0.095744684,"top":0.10574621,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (31)","depth":12,"bounds":{"left":0.11269947,"top":0.09936153,"width":0.054521278,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"bounds":{"left":0.12333777,"top":0.10574621,"width":0.02925532,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"bounds":{"left":0.15525267,"top":0.113727055,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"31","depth":14,"bounds":{"left":0.15824468,"top":0.113727055,"width":0.004986702,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"bounds":{"left":0.16323139,"top":0.113727055,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"bounds":{"left":0.16988032,"top":0.09936153,"width":0.029089095,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"bounds":{"left":0.18085106,"top":0.10574621,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"bounds":{"left":0.20162898,"top":0.09936153,"width":0.03025266,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"bounds":{"left":0.21276596,"top":0.10574621,"width":0.015957447,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"bounds":{"left":0.23454122,"top":0.09936153,"width":0.022938829,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"bounds":{"left":0.24534574,"top":0.10574621,"width":0.009142287,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (21)","depth":12,"bounds":{"left":0.2601396,"top":0.09936153,"width":0.069980055,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"bounds":{"left":0.27194148,"top":0.10574621,"width":0.04255319,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"bounds":{"left":0.31831783,"top":0.113727055,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"21","depth":14,"bounds":{"left":0.32130983,"top":0.113727055,"width":0.0048204786,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"bounds":{"left":0.32613033,"top":0.113727055,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"bounds":{"left":0.33277926,"top":0.09936153,"width":0.03125,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"bounds":{"left":0.34391624,"top":0.10574621,"width":0.016788565,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"bounds":{"left":0.36668882,"top":0.09936153,"width":0.032081116,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"bounds":{"left":0.3778258,"top":0.10574621,"width":0.01761968,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"bounds":{"left":0.09325133,"top":0.14365523,"width":0.0003324468,"height":0.016759777},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"bounds":{"left":0.09325133,"top":0.1452514,"width":0.039228722,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"bounds":{"left":0.09325133,"top":0.1452514,"width":0.2159242,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"bounds":{"left":0.30917552,"top":0.1452514,"width":0.04055851,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"bounds":{"left":0.30917552,"top":0.1452514,"width":0.04055851,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"bounds":{"left":0.34973404,"top":0.1452514,"width":0.08261303,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"bounds":{"left":0.4323471,"top":0.1452514,"width":0.05219415,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"bounds":{"left":0.4323471,"top":0.1452514,"width":0.05219415,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"bounds":{"left":0.48454124,"top":0.1452514,"width":0.0013297872,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"bounds":{"left":0.98636967,"top":0.13886672,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Review requested","depth":15,"bounds":{"left":0.35006648,"top":0.2019154,"width":0.0003324468,"height":0.016759777},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Review requested","depth":16,"bounds":{"left":0.35006648,"top":0.20351157,"width":0.039893616,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"bounds":{"left":0.35006648,"top":0.20351157,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"bounds":{"left":0.35006648,"top":0.20351157,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requested your review on this pull request.","depth":15,"bounds":{"left":0.3854721,"top":0.20351157,"width":0.09158909,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add your review","depth":14,"bounds":{"left":0.70212764,"top":0.19872306,"width":0.036901597,"height":0.022346368},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add your review","depth":16,"bounds":{"left":0.70511967,"top":0.20391062,"width":0.030917553,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title","depth":13,"bounds":{"left":0.33776596,"top":0.24221867,"width":0.26230052,"height":0.031923383},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects","depth":14,"bounds":{"left":0.33776596,"top":0.24301676,"width":0.2122673,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"bounds":{"left":0.55269283,"top":0.24301676,"width":0.006482713,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11989","depth":15,"bounds":{"left":0.55917555,"top":0.24301676,"width":0.028922873,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"bounds":{"left":0.5894282,"top":0.24541101,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Awaiting approval","depth":13,"bounds":{"left":0.6569149,"top":0.24860336,"width":0.055518616,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Awaiting approval","depth":15,"bounds":{"left":0.66921544,"top":0.254589,"width":0.038896278,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"bounds":{"left":0.7137633,"top":0.24860336,"width":0.02825798,"height":0.025538707},"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"bounds":{"left":0.7180851,"top":0.254589,"width":0.011635638,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"bounds":{"left":0.34840426,"top":0.28651237,"width":0.011968086,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"bounds":{"left":0.36702126,"top":0.28332004,"width":0.034075797,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"bounds":{"left":0.36702126,"top":0.2849162,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 22 commits into","depth":15,"bounds":{"left":0.40242687,"top":0.2849162,"width":0.069148935,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"bounds":{"left":0.47290558,"top":0.282921,"width":0.018284574,"height":0.017557861},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"bounds":{"left":0.47490028,"top":0.28611332,"width":0.014295213,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"bounds":{"left":0.49251994,"top":0.2849162,"width":0.009973404,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701-reschedule-HubSpot-processing","depth":16,"bounds":{"left":0.50382316,"top":0.282921,"width":0.09524601,"height":0.017557861},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701-reschedule-HubSpot-processing","depth":17,"bounds":{"left":0.50581783,"top":0.28611332,"width":0.09125665,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"bounds":{"left":0.60039896,"top":0.28052673,"width":0.00930851,"height":0.022346368},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 949 additions & 97 deletions","depth":14,"bounds":{"left":0.7067819,"top":0.3367917,"width":0.019946808,"height":0.11412609},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (5)","depth":16,"bounds":{"left":0.33776596,"top":0.31883478,"width":0.057347074,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"bounds":{"left":0.35139626,"top":0.32841182,"width":0.028091755,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.38946143,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5","depth":18,"bounds":{"left":0.39245346,"top":0.32841182,"width":0.0028257978,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.39527926,"top":0.32841182,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (22)","depth":16,"bounds":{"left":0.39511302,"top":0.31883478,"width":0.05069814,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"bounds":{"left":0.40874335,"top":0.32841182,"width":0.019115692,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.4401596,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22","depth":18,"bounds":{"left":0.4431516,"top":0.32841182,"width":0.005485372,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.44863698,"top":0.32841182,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (3)","depth":16,"bounds":{"left":0.44581118,"top":0.31883478,"width":0.04504654,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"bounds":{"left":0.45944148,"top":0.32841182,"width":0.015957447,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.48520613,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"bounds":{"left":0.48819813,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.49119017,"top":0.32841182,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (11)","depth":16,"bounds":{"left":0.49085772,"top":0.31883478,"width":0.060339097,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"bounds":{"left":0.50448805,"top":0.32841182,"width":0.029753989,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.5455452,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11","depth":18,"bounds":{"left":0.54853725,"top":0.32841182,"width":0.0043218085,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.55285907,"top":0.32841182,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Conversation","depth":12,"bounds":{"left":0.33776596,"top":0.3643256,"width":0.0003324468,"height":0.0007980846},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"bounds":{"left":0.33776596,"top":0.36711892,"width":0.048204787,"height":0.023144454},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":12,"bounds":{"left":0.33776596,"top":0.3643256,"width":0.013297873,"height":0.031923383},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"bounds":{"left":0.61136967,"top":0.3651237,"width":0.007978723,"height":0.02952913},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago •","depth":14,"bounds":{"left":0.3620346,"top":0.3651237,"width":0.24135639,"height":0.02952913},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":16,"bounds":{"left":0.3620346,"top":0.37310454,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":17,"bounds":{"left":0.3620346,"top":0.37310454,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"bounds":{"left":0.39744017,"top":0.37310454,"width":0.025598405,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":15,"bounds":{"left":0.42436835,"top":0.3715084,"width":0.023603724,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":17,"bounds":{"left":0.42436835,"top":0.37310454,"width":0.023603724,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"bounds":{"left":0.44930187,"top":0.37310454,"width":0.0021609042,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"bounds":{"left":0.45262632,"top":0.3715084,"width":0.020279255,"height":0.016759777},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"bounds":{"left":0.45262632,"top":0.37310454,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JIRA: JY-20701","depth":16,"bounds":{"left":0.3620346,"top":0.40822026,"width":0.25731382,"height":0.017557861},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JIRA:","depth":17,"bounds":{"left":0.3620346,"top":0.4086193,"width":0.015791224,"height":0.016759777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701","depth":17,"bounds":{"left":0.3778258,"top":0.4086193,"width":0.026263298,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701","depth":18,"bounds":{"left":0.3778258,"top":0.4086193,"width":0.026263298,"height":0.016759777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Casus:","depth":16,"bounds":{"left":0.3620346,"top":0.44493216,"width":0.25731382,"height":0.01396648},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Casus:","depth":17,"bounds":{"left":0.3620346,"top":0.44493216,"width":0.015292553,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"We want more fluent curve of the jobs distribution (no spikes) in","depth":18,"bounds":{"left":0.3700133,"top":0.47326416,"width":0.13879654,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm-sync","depth":19,"bounds":{"left":0.51047206,"top":0.47525936,"width":0.018949468,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"queue","depth":18,"bounds":{"left":0.53108376,"top":0.47326416,"width":0.01462766,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times","depth":18,"bounds":{"left":0.3700133,"top":0.49281725,"width":0.24750665,"height":0.030726258},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Changes:","depth":16,"bounds":{"left":0.3620346,"top":0.5442937,"width":0.25731382,"height":0.01396648},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changes:","depth":17,"bounds":{"left":0.3620346,"top":0.5442937,"width":0.021110373,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Remove HubSpot from SyncObjects","depth":18,"bounds":{"left":0.3700133,"top":0.5726257,"width":0.07712766,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Create separate HubSpotSyncObjects that runs each 5 min instead of each 30","depth":18,"bounds":{"left":0.3700133,"top":0.59217876,"width":0.16855054,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Extract common code","depth":18,"bounds":{"left":0.3700133,"top":0.6121309,"width":0.047041222,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Improve processing Webhooks jobs distribution","depth":18,"bounds":{"left":0.3700133,"top":0.63168395,"width":0.101894945,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Optimize ImportOpportunityBatch:","depth":18,"bounds":{"left":0.3700133,"top":0.65163606,"width":0.07396942,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cache OpportunitySyncableFields, OwnerProfiles, RecordType","depth":20,"bounds":{"left":0.37799203,"top":0.6683959,"width":0.13347739,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"remove findContactByConfigurationAndId - Contact ID already exist","depth":20,"bounds":{"left":0.37799203,"top":0.68834794,"width":0.1456117,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"add timings log","depth":20,"bounds":{"left":0.37799203,"top":0.70790106,"width":0.032912236,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":16,"bounds":{"left":0.3620346,"top":0.73623306,"width":0.008643617,"height":0.0207502},"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":13,"bounds":{"left":0.33776596,"top":0.7960894,"width":0.013297873,"height":0.031923383},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"bounds":{"left":0.61136967,"top":0.79688746,"width":0.007978723,"height":0.02952913},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Author","depth":15,"bounds":{"left":0.5934175,"top":0.80606544,"width":0.012965426,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago","depth":13,"bounds":{"left":0.3620346,"top":0.79688746,"width":0.22240691,"height":0.02952913},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"bounds":{"left":0.3620346,"top":0.80486834,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"bounds":{"left":0.3620346,"top":0.80486834,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"bounds":{"left":0.39744017,"top":0.80486834,"width":0.025598405,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"bounds":{"left":0.42436835,"top":0.8032721,"width":0.023603724,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"bounds":{"left":0.42436835,"top":0.80486834,"width":0.023603724,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":17,"bounds":{"left":0.3620346,"top":0.8415802,"width":0.019115692,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@claude","depth":18,"bounds":{"left":0.3620346,"top":0.8415802,"width":0.019115692,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"bounds":{"left":0.3620346,"top":0.86951315,"width":0.008643617,"height":0.0207502},"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"react with eyes","depth":14,"bounds":{"left":0.37200797,"top":0.86951315,"width":0.013796543,"height":0.0207502},"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"👀","depth":16,"bounds":{"left":0.37416887,"top":0.8747007,"width":0.004155585,"height":0.012769354},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":16,"bounds":{"left":0.38098404,"top":0.8747007,"width":0.0018284575,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":13,"bounds":{"left":0.33776596,"top":0.9293695,"width":0.013297873,"height":0.031923383},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"bounds":{"left":0.61136967,"top":0.9301676,"width":0.007978723,"height":0.02952913},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"claude bot commented 4 days ago •","depth":13,"bounds":{"left":0.3620346,"top":0.9301676,"width":0.24135639,"height":0.029928172},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"claude","depth":15,"bounds":{"left":0.3620346,"top":0.93814844,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"claude","depth":16,"bounds":{"left":0.3620346,"top":0.93814844,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":16,"bounds":{"left":0.3804854,"top":0.9397446,"width":0.006482713,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"bounds":{"left":0.390625,"top":0.93814844,"width":0.02543218,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"bounds":{"left":0.41738698,"top":0.9365523,"width":0.023603724,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"bounds":{"left":0.41738698,"top":0.93814844,"width":0.023603724,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":16,"bounds":{"left":0.44232047,"top":0.93814844,"width":0.0021609042,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":16,"bounds":{"left":0.44581118,"top":0.9365523,"width":0.020113032,"height":0.016759777},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":18,"bounds":{"left":0.44581118,"top":0.93814844,"width":0.014793883,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Claude finished","depth":18,"bounds":{"left":0.3620346,"top":0.97525936,"width":0.036070477,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":18,"bounds":{"left":0.39810506,"top":0.97525936,"width":0.038397606,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@yalokin-jiminny","depth":19,"bounds":{"left":0.39810506,"top":0.97525936,"width":0.038397606,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s task in 1m 59s","depth":18,"bounds":{"left":0.43650267,"top":0.97525936,"width":0.03756649,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"——","depth":17,"bounds":{"left":0.47406915,"top":0.97525936,"width":0.010638298,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"View job","depth":17,"bounds":{"left":0.48470744,"top":0.97525936,"width":0.018284574,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"View job","depth":18,"bounds":{"left":0.48470744,"top":0.97525936,"width":0.018284574,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Code Review","depth":16,"bounds":{"left":0.3620346,"top":1.0,"width":0.25731382,"height":-0.0315243},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Code Review","depth":17,"bounds":{"left":0.3620346,"top":1.0,"width":0.03523936,"height":-0.031923413},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall the approach is clean — extracting the shared logic into","depth":17,"bounds":{"left":0.3620346,"top":1.0,"width":0.13696809,"height":-0.06304872},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsCommandTrait","depth":18,"bounds":{"left":0.5006649,"top":1.0,"width":0.0546875,"height":-0.065043926},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is a good call, and the scheduling design (offset cron at","depth":17,"bounds":{"left":0.3620346,"top":1.0,"width":0.24335106,"height":-0.06304872},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":01","depth":18,"bounds":{"left":0.43567154,"top":1.0,"width":0.0071476065,"height":-0.08180368},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to avoid spikes) makes sense. I found two bugs and a few minor points.","depth":17,"bounds":{"left":0.44448137,"top":1.0,"width":0.15425532,"height":-0.07980847},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Bugs","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bugs","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. Null dereference on","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:49-51","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"$team->crm","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"can be null if the team has no CRM configuration record. Accessing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->provider","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"on null will throw a fatal error. The existing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"job handles this by using","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"??","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig->provider ?? 'unknown'","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). The new job needs a null guard here:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"===","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"||","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"logger","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"warning","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[SyncHubspotObjects] Team has no HubSpot CRM config","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", [","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=>","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]);","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"return","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";\n}","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Fix this →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$lastSyncedAt","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is nullable but","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requires","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:85,88,116","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"last_synced_at","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// can be null on first run","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// ...","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"private","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"function","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"HubspotInterface","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"):","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"void","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"With","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"declare(strict_types=1)","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", passing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"-typed parameter throws a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TypeError","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". A team that has never synced before will crash on the first run. The parameter should be","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"?Carbon","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(and","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncOpportunities","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"should handle null), or provide a fallback like","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon::createFromTimestamp(0)","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Fix this →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Minor Issues","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Minor Issues","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. Double call to","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix()","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"prefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() ?","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() .","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"''","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"getLogPrefix()","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is called twice. Trivial but unnecessary — capture the result in a variable first.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"command doesn't guard HubSpot when","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{team}","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"argument is given —","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/SyncObjects.php:40-41","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teams","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[] = Team::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"idOrUuId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// no provider check","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Running","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-objects <hubspot-team-id>","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dispatches a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsJob","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Design Notes","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Design Notes","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Scheduler placement","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is placed inside","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"scheduleEveryFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"but uses a manual cron","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"instead of","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->everyFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SQS visibility timeout","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncHubspotObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"uses 1 hour (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"60 * 60","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") vs","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s 6 hours (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"60 * 60 * 6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). Appropriate given the more frequent, presumably lighter runs.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Stagger delay","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"yalokin-jiminny","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"force-pushed","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"force-pushed","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"the","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20553-debug-crm-sync-delays","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"branch from","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"f6c0ec3","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"f6c0ec3","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"5928f60","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5928f60","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Compare","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Compare","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yesterday","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yesterday","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"yalokin-jiminny","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"force-pushed","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"force-pushed","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"the","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20701-reschedule-HubSpot-processing","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"branch from","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"3d7234a","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"3d7234a","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1d10796","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1d10796","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Compare","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Compare","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yesterday","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yesterday","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Base automatically changed from","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20553-debug-crm-sync-delays","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"master","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"19 hours ago","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
9114085967498441256
|
8380486261257862477
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (
60 * 60 * 6
). Appropriate given the more frequent, presumably lighter runs.
Stagger delay
: HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.
Add or remove reactions
@yalokin-jiminny
yalokin-jiminny
yalokin-jiminny
force-pushed
force-pushed
the
JY-20553-debug-crm-sync-delays
branch from
f6c0ec3
f6c0ec3
to
5928f60
5928f60
Compare
Compare
yesterday
yesterday
@yalokin-jiminny
yalokin-jiminny
yalokin-jiminny
force-pushed
force-pushed
the
JY-20701-reschedule-HubSpot-processing
branch from
3d7234a
3d7234a
to
1d10796
1d10796
Compare
Compare
yesterday
yesterday
Base automatically changed from
JY-20553-debug-crm-sync-delays
to
master
19 hours ago...
|
NULL
|
|
60919
|
1312
|
30
|
2026-04-21T06:25:20.886967+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-21/1776 /Users/lukas/.screenpipe/data/data/2026-04-21/1776752720886_m1.jpg...
|
Firefox
|
JY-20701 | Reschedule HubSpot Sync Objects by yalo JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11989
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (
60 * 60 * 6
). Appropriate given the more frequent, presumably lighter runs.
Stagger delay
: HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.
Add or remove reactions...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny MCP Connector - Product - Confluence","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny MCP Connector - Product - Confluence","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny Mail","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny Mail","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Feed — jiminny — Sentry","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Feed — jiminny — Sentry","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"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,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (31)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"31","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (21)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"21","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Review requested","depth":15,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Review requested","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requested your review on this pull request.","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add your review","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add your review","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11989","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Awaiting approval","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Awaiting approval","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 22 commits into","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701-reschedule-HubSpot-processing","depth":16,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701-reschedule-HubSpot-processing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 949 additions & 97 deletions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (5)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (22)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (11)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Conversation","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago •","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JIRA: JY-20701","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JIRA:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Casus:","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Casus:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"We want more fluent curve of the jobs distribution (no spikes) in","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm-sync","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"queue","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Changes:","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changes:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Remove HubSpot from SyncObjects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Create separate HubSpotSyncObjects that runs each 5 min instead of each 30","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Extract common code","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Improve processing Webhooks jobs distribution","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Optimize ImportOpportunityBatch:","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cache OpportunitySyncableFields, OwnerProfiles, RecordType","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"remove findContactByConfigurationAndId - Contact ID already exist","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"add timings log","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":16,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":13,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Author","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@claude","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"react with eyes","depth":14,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"👀","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":13,"bounds":{"left":0.14097223,"top":0.0,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"bounds":{"left":0.7125,"top":0.0,"width":0.016666668,"height":0.04111111},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"claude bot commented 4 days ago •","depth":13,"bounds":{"left":0.19166666,"top":0.0,"width":0.50416666,"height":0.041666668},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"claude","depth":15,"bounds":{"left":0.19166666,"top":0.0,"width":0.03125,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"claude","depth":16,"bounds":{"left":0.19166666,"top":0.0,"width":0.03125,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":16,"bounds":{"left":0.23020834,"top":0.0,"width":0.013541667,"height":0.016666668},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"bounds":{"left":0.25138888,"top":0.0,"width":0.053125,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"bounds":{"left":0.30729166,"top":0.0,"width":0.049305554,"height":0.023333333},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"bounds":{"left":0.30729166,"top":0.0,"width":0.049305554,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":16,"bounds":{"left":0.359375,"top":0.0,"width":0.004513889,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":16,"bounds":{"left":0.36666667,"top":0.0,"width":0.042013887,"height":0.023333333},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":18,"bounds":{"left":0.36666667,"top":0.0,"width":0.030902777,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Claude finished","depth":18,"bounds":{"left":0.19166666,"top":0.0,"width":0.07534722,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":18,"bounds":{"left":0.26701388,"top":0.0,"width":0.08020833,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@yalokin-jiminny","depth":19,"bounds":{"left":0.26701388,"top":0.0,"width":0.08020833,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s task in 1m 59s","depth":18,"bounds":{"left":0.3472222,"top":0.0,"width":0.07847222,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"——","depth":17,"bounds":{"left":0.42569444,"top":0.0,"width":0.022222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"View job","depth":17,"bounds":{"left":0.44791666,"top":0.0,"width":0.038194444,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"View job","depth":18,"bounds":{"left":0.44791666,"top":0.0,"width":0.038194444,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Code Review","depth":16,"bounds":{"left":0.19166666,"top":0.04388889,"width":0.5375,"height":0.02388889},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Code Review","depth":17,"bounds":{"left":0.19166666,"top":0.044444446,"width":0.07361111,"height":0.023333333},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall the approach is clean — extracting the shared logic into","depth":17,"bounds":{"left":0.19166666,"top":0.08777778,"width":0.28611112,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsCommandTrait","depth":18,"bounds":{"left":0.48125,"top":0.090555556,"width":0.11423611,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is a good call, and the scheduling design (offset cron at","depth":17,"bounds":{"left":0.19166666,"top":0.08777778,"width":0.5083333,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":01","depth":18,"bounds":{"left":0.3454861,"top":0.11388889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to avoid spikes) makes sense. I found two bugs and a few minor points.","depth":17,"bounds":{"left":0.3638889,"top":0.11111111,"width":0.32222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Bugs","depth":16,"bounds":{"left":0.19166666,"top":0.18944444,"width":0.5375,"height":0.024444444},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bugs","depth":17,"bounds":{"left":0.19166666,"top":0.19,"width":0.028819444,"height":0.023333333},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. Null dereference on","depth":18,"bounds":{"left":0.19166666,"top":0.2338889,"width":0.10451389,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig","depth":19,"bounds":{"left":0.29965279,"top":0.23666666,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"bounds":{"left":0.35277778,"top":0.2338889,"width":0.013541667,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:49-51","depth":19,"bounds":{"left":0.36979166,"top":0.23666666,"width":0.20416667,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.29222223,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.20763889,"top":0.29222223,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.25243056,"top":0.29222223,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.2673611,"top":0.29222223,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.27222222,"top":0.29222223,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.2923611,"top":0.29222223,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"bounds":{"left":0.30208334,"top":0.29222223,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.3170139,"top":0.29222223,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"bounds":{"left":0.20277777,"top":0.33055556,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.2125,"top":0.33055556,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.33055556,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.22743055,"top":0.33055556,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.27222222,"top":0.33055556,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"bounds":{"left":0.28229168,"top":0.33055556,"width":0.039930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"bounds":{"left":0.32222223,"top":0.33055556,"width":0.099305555,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"bounds":{"left":0.42152777,"top":0.33055556,"width":0.07986111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"bounds":{"left":0.5013889,"top":0.33055556,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.28166667,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"$team->crm","depth":18,"bounds":{"left":0.19479166,"top":0.3888889,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"can be null if the team has no CRM configuration record. Accessing","depth":17,"bounds":{"left":0.24791667,"top":0.3861111,"width":0.30729166,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->provider","depth":18,"bounds":{"left":0.55833334,"top":0.3888889,"width":0.05,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"on null will throw a fatal error. The existing","depth":17,"bounds":{"left":0.19166666,"top":0.3861111,"width":0.5277778,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":18,"bounds":{"left":0.27881944,"top":0.41222224,"width":0.05451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"job handles this by using","depth":17,"bounds":{"left":0.33680555,"top":0.40944445,"width":0.11666667,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"??","depth":18,"bounds":{"left":0.45694444,"top":0.41222224,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.47013888,"top":0.40944445,"width":0.00625,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig->provider ?? 'unknown'","depth":18,"bounds":{"left":0.4798611,"top":0.41222224,"width":0.16423611,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). The new job needs a null guard here:","depth":17,"bounds":{"left":0.19166666,"top":0.40944445,"width":0.51944447,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.49055555,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.20763889,"top":0.49055555,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.25243056,"top":0.49055555,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.2673611,"top":0.49055555,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.27222222,"top":0.49055555,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.2923611,"top":0.49055555,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"bounds":{"left":0.30208334,"top":0.49055555,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.3170139,"top":0.49055555,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"bounds":{"left":0.20277777,"top":0.5288889,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.2125,"top":0.5288889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.5288889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.22743055,"top":0.5288889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"===","depth":17,"bounds":{"left":0.27222222,"top":0.5288889,"width":0.025,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":17,"bounds":{"left":0.29722223,"top":0.5288889,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"||","depth":17,"bounds":{"left":0.3170139,"top":0.5288889,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.33715278,"top":0.5288889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.3420139,"top":0.5288889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.38680556,"top":0.5288889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"bounds":{"left":0.396875,"top":0.5288889,"width":0.039583333,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"bounds":{"left":0.43645832,"top":0.5288889,"width":0.099652775,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"bounds":{"left":0.5361111,"top":0.5288889,"width":0.07951389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"bounds":{"left":0.20277777,"top":0.5288889,"width":0.42777777,"height":0.035555556},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"logger","depth":17,"bounds":{"left":0.22743055,"top":0.54833335,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.25729167,"top":0.54833335,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"warning","depth":17,"bounds":{"left":0.2673611,"top":0.54833335,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.30208334,"top":0.54833335,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.30729166,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[SyncHubspotObjects] Team has no HubSpot CRM config","depth":17,"bounds":{"left":0.31215277,"top":0.54833335,"width":0.25381944,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.5659722,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", [","depth":17,"bounds":{"left":0.5708333,"top":0.54833335,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.5857639,"top":0.54833335,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.59097224,"top":0.54833335,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.6107639,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=>","depth":17,"bounds":{"left":0.615625,"top":0.54833335,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.6357639,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"bounds":{"left":0.640625,"top":0.54833335,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.66041666,"top":0.54833335,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"bounds":{"left":0.6704861,"top":0.54833335,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]);","depth":17,"bounds":{"left":0.20277777,"top":0.54833335,"width":0.5125,"height":0.035},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"return","depth":17,"bounds":{"left":0.22256945,"top":0.56722224,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";\n}","depth":17,"bounds":{"left":0.20277777,"top":0.56722224,"width":0.05451389,"height":0.035555556},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.48055556,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Fix this →","depth":17,"bounds":{"left":0.19166666,"top":0.6422222,"width":0.043055557,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"bounds":{"left":0.19166666,"top":0.6422222,"width":0.043055557,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.","depth":18,"bounds":{"left":0.19166666,"top":0.7227778,"width":0.011458334,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$lastSyncedAt","depth":19,"bounds":{"left":0.20659722,"top":0.72555554,"width":0.06458333,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is nullable but","depth":18,"bounds":{"left":0.27430555,"top":0.7227778,"width":0.07083333,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":19,"bounds":{"left":0.34861112,"top":0.72555554,"width":0.10451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requires","depth":18,"bounds":{"left":0.45625,"top":0.7227778,"width":0.04375,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":19,"bounds":{"left":0.5034722,"top":0.72555554,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"bounds":{"left":0.5364583,"top":0.7227778,"width":0.013888889,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:85,88,116","depth":19,"bounds":{"left":0.19166666,"top":0.72555554,"width":0.42673612,"height":0.039444443},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.8038889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.20763889,"top":0.8038889,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.2673611,"top":0.8038889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.28229168,"top":0.8038889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.28715277,"top":0.8038889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.33194444,"top":0.8038889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"last_synced_at","depth":17,"bounds":{"left":0.3420139,"top":0.8038889,"width":0.06979167,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.41180557,"top":0.8038889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// can be null on first run","depth":17,"bounds":{"left":0.42673612,"top":0.8038889,"width":0.134375,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.20277777,"top":0.8233333,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.20763889,"top":0.8233333,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.2125,"top":0.8233333,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"bounds":{"left":0.20763889,"top":0.8422222,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.22743055,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":17,"bounds":{"left":0.2375,"top":0.8422222,"width":0.10451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.3420139,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.346875,"top":0.8422222,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"bounds":{"left":0.35208333,"top":0.8422222,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"bounds":{"left":0.4017361,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.41180557,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.41666666,"top":0.8422222,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"bounds":{"left":0.4763889,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// ...","depth":17,"bounds":{"left":0.20277777,"top":0.88055557,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"private","depth":17,"bounds":{"left":0.20277777,"top":0.9,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"function","depth":17,"bounds":{"left":0.24236111,"top":0.9,"width":0.039930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects(","depth":17,"bounds":{"left":0.28229168,"top":0.9,"width":0.114583336,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"HubspotInterface","depth":17,"bounds":{"left":0.396875,"top":0.9,"width":0.07951389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.48125,"top":0.9,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"bounds":{"left":0.48645833,"top":0.9,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"bounds":{"left":0.5361111,"top":0.9,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":17,"bounds":{"left":0.54618055,"top":0.9,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.58090276,"top":0.9,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.5857639,"top":0.9,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"):","depth":17,"bounds":{"left":0.6454861,"top":0.9,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"void","depth":17,"bounds":{"left":0.66041666,"top":0.9,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.79388887,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"With","depth":17,"bounds":{"left":0.19166666,"top":0.95555556,"width":0.023263888,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"declare(strict_types=1)","depth":18,"bounds":{"left":0.21805556,"top":0.9583333,"width":0.114583336,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", passing","depth":17,"bounds":{"left":0.3361111,"top":0.95555556,"width":0.042708334,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":18,"bounds":{"left":0.38229167,"top":0.9583333,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to a","depth":17,"bounds":{"left":0.40555555,"top":0.95555556,"width":0.022222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":18,"bounds":{"left":0.43125,"top":0.9583333,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"-typed parameter throws a","depth":17,"bounds":{"left":0.4642361,"top":0.95555556,"width":0.12256944,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TypeError","depth":18,"bounds":{"left":0.5902778,"top":0.9583333,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". A team that has never synced before will crash on the first run. The parameter should be","depth":17,"bounds":{"left":0.19166666,"top":0.95555556,"width":0.5229167,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"?Carbon","depth":18,"bounds":{"left":0.52118057,"top":0.9816667,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(and","depth":17,"bounds":{"left":0.559375,"top":0.97888887,"width":0.025694445,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncOpportunities","depth":18,"bounds":{"left":0.5885417,"top":0.9816667,"width":0.084375,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"should handle null), or provide a fallback like","depth":17,"bounds":{"left":0.19166666,"top":0.97888887,"width":0.5173611,"height":0.02111113},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon::createFromTimestamp(0)","depth":18,"bounds":{"left":0.36423612,"top":1.0,"width":0.14930555,"height":-0.004999995},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.51666665,"top":1.0,"width":0.0027777778,"height":-0.0022221804},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Fix this →","depth":17,"bounds":{"left":0.19166666,"top":1.0,"width":0.043055557,"height":-0.043333292},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"bounds":{"left":0.19166666,"top":1.0,"width":0.043055557,"height":-0.043333292},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Minor Issues","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Minor Issues","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. Double call to","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix()","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"prefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() ?","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() .","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"''","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"getLogPrefix()","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is called twice. Trivial but unnecessary — capture the result in a variable first.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"command doesn't guard HubSpot when","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{team}","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"argument is given —","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/SyncObjects.php:40-41","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teams","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[] = Team::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"idOrUuId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// no provider check","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Running","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-objects <hubspot-team-id>","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dispatches a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsJob","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Design Notes","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Design Notes","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Scheduler placement","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is placed inside","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"scheduleEveryFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"but uses a manual cron","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"instead of","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->everyFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SQS visibility timeout","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncHubspotObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"uses 1 hour (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"60 * 60","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") vs","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s 6 hours (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"60 * 60 * 6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). Appropriate given the more frequent, presumably lighter runs.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Stagger delay","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1828305760214103697
|
8380486261237022029
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (
60 * 60 * 6
). Appropriate given the more frequent, presumably lighter runs.
Stagger delay
: HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.
Add or remove reactions...
|
60917
|
|
62775
|
1353
|
29
|
2026-04-21T08:06:16.099480+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-21/1776 /Users/lukas/.screenpipe/data/data/2026-04-21/1776758776099_m1.jpg...
|
Firefox
|
JY-20701 | Reschedule HubSpot Sync Objects by yalo JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11989#pullrequestrevie github.com/jiminny/app/pull/11989#pullrequestreview-4145841515...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
Project Phoenix – Figma
Project Phoenix – Figma
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
Pipelines - jiminny/app
Pipelines - jiminny/app
Formalize
Formalize
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (30)
Pull requests
(
30
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Checks pending
Checks pending
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Open
JY-20701 | Reschedule HubSpot Sync Objects #11989 yalokin-jiminny wants to merge 22 commits into master from JY-20701-reschedule-HubSpot-processing Copy head branch name to clipboard
JY-20701 | Reschedule HubSpot Sync Objects
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Project Phoenix – Figma","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Project Phoenix – Figma","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny MCP Connector - Product - Confluence","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny MCP Connector - Product - Confluence","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny Mail","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny Mail","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Feed — jiminny — Sentry","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Feed — jiminny — Sentry","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Formalize","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Formalize","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"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,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (30)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"30","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (21)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"21","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11989","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Checks pending","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks pending","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 22 commits into","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701-reschedule-HubSpot-processing","depth":16,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701-reschedule-HubSpot-processing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 949 additions & 97 deletions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (5)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (22)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (11)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JY-20701 | Reschedule HubSpot Sync Objects #11989 yalokin-jiminny wants to merge 22 commits into master from JY-20701-reschedule-HubSpot-processing Copy head branch name to clipboard","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701 | Reschedule HubSpot Sync Objects","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11989","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":18,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 22 commits into","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":18,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701-reschedule-HubSpot-processing","depth":19,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701-reschedule-HubSpot-processing","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":19,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Conversation","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago •","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JIRA: JY-20701","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JIRA:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Casus:","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Casus:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"We want more fluent curve of the jobs distribution (no spikes) in","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm-sync","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"queue","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Changes:","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changes:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Remove HubSpot from SyncObjects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Create separate HubSpotSyncObjects that runs each 5 min instead of each 30","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Extract common code","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Improve processing Webhooks jobs distribution","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Optimize ImportOpportunityBatch:","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cache OpportunitySyncableFields, OwnerProfiles, RecordType","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"remove findContactByConfigurationAndId - Contact ID already exist","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"add timings log","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":16,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":13,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Author","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@claude","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"react with eyes","depth":14,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"👀","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":13,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"claude bot commented 4 days ago •","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"claude","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"claude","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Claude finished","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":18,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@yalokin-jiminny","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s task in 1m 59s","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"——","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"View job","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"View job","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Code Review","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Code Review","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall the approach is clean — extracting the shared logic into","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsCommandTrait","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is a good call, and the scheduling design (offset cron at","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":01","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to avoid spikes) makes sense. I found two bugs and a few minor points.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Bugs","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bugs","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. Null dereference on","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:49-51","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"$team->crm","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"can be null if the team has no CRM configuration record. Accessing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->provider","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"on null will throw a fatal error. The existing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"job handles this by using","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"??","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig->provider ?? 'unknown'","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). The new job needs a null guard here:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"===","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"||","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"logger","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"warning","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[SyncHubspotObjects] Team has no HubSpot CRM config","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", [","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=>","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]);","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"return","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";\n}","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Fix this →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$lastSyncedAt","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is nullable but","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requires","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:85,88,116","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"last_synced_at","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// can be null on first run","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// ...","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"private","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"function","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"HubspotInterface","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"):","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"void","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"With","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"declare(strict_types=1)","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", passing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"-typed parameter throws a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TypeError","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". A team that has never synced before will crash on the first run. The parameter should be","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"?Carbon","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(and","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncOpportunities","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"should handle null), or provide a fallback like","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon::createFromTimestamp(0)","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Fix this →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Minor Issues","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Minor Issues","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. Double call to","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix()","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"prefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() ?","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() .","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"''","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"getLogPrefix()","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is called twice. Trivial but unnecessary — capture the result in a variable first.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-3285429829612070228
|
8380486227443509719
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
Project Phoenix – Figma
Project Phoenix – Figma
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
Pipelines - jiminny/app
Pipelines - jiminny/app
Formalize
Formalize
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (30)
Pull requests
(
30
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Checks pending
Checks pending
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Open
JY-20701 | Reschedule HubSpot Sync Objects #11989 yalokin-jiminny wants to merge 22 commits into master from JY-20701-reschedule-HubSpot-processing Copy head branch name to clipboard
JY-20701 | Reschedule HubSpot Sync Objects
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects...
|
62774
|
|
60924
|
1312
|
31
|
2026-04-21T06:25:41.861900+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-21/1776 /Users/lukas/.screenpipe/data/data/2026-04-21/1776752741861_m1.jpg...
|
Firefox
|
JY-20701 | Reschedule HubSpot Sync Objects by yalo JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11989
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny MCP Connector - Product - Confluence","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny MCP Connector - Product - Confluence","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny Mail","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny Mail","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Feed — jiminny — Sentry","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Feed — jiminny — Sentry","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"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,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (31)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"31","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (21)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"21","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Review requested","depth":15,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Review requested","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requested your review on this pull request.","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add your review","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add your review","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11989","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Awaiting approval","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Awaiting approval","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 22 commits into","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701-reschedule-HubSpot-processing","depth":16,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701-reschedule-HubSpot-processing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 949 additions & 97 deletions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (5)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (22)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (11)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Conversation","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago •","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JIRA: JY-20701","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JIRA:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Casus:","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Casus:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"We want more fluent curve of the jobs distribution (no spikes) in","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm-sync","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"queue","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Changes:","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changes:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Remove HubSpot from SyncObjects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Create separate HubSpotSyncObjects that runs each 5 min instead of each 30","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Extract common code","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Improve processing Webhooks jobs distribution","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Optimize ImportOpportunityBatch:","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cache OpportunitySyncableFields, OwnerProfiles, RecordType","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"remove findContactByConfigurationAndId - Contact ID already exist","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"add timings log","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":16,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":13,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Author","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@claude","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"react with eyes","depth":14,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"👀","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":13,"bounds":{"left":0.14097223,"top":0.0,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"bounds":{"left":0.7125,"top":0.0,"width":0.016666668,"height":0.04111111},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"claude bot commented 4 days ago •","depth":13,"bounds":{"left":0.19166666,"top":0.0,"width":0.50416666,"height":0.041666668},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"claude","depth":15,"bounds":{"left":0.19166666,"top":0.0,"width":0.03125,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"claude","depth":16,"bounds":{"left":0.19166666,"top":0.0,"width":0.03125,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":16,"bounds":{"left":0.23020834,"top":0.0,"width":0.013541667,"height":0.016666668},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"bounds":{"left":0.25138888,"top":0.0,"width":0.053125,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"bounds":{"left":0.30729166,"top":0.0,"width":0.049305554,"height":0.023333333},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"bounds":{"left":0.30729166,"top":0.0,"width":0.049305554,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":16,"bounds":{"left":0.359375,"top":0.0,"width":0.004513889,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":16,"bounds":{"left":0.36666667,"top":0.0,"width":0.042013887,"height":0.023333333},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":18,"bounds":{"left":0.36666667,"top":0.0,"width":0.030902777,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Claude finished","depth":18,"bounds":{"left":0.19166666,"top":0.0,"width":0.07534722,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":18,"bounds":{"left":0.26701388,"top":0.0,"width":0.08020833,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@yalokin-jiminny","depth":19,"bounds":{"left":0.26701388,"top":0.0,"width":0.08020833,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s task in 1m 59s","depth":18,"bounds":{"left":0.3472222,"top":0.0,"width":0.07847222,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"——","depth":17,"bounds":{"left":0.42569444,"top":0.0,"width":0.022222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"View job","depth":17,"bounds":{"left":0.44791666,"top":0.0,"width":0.038194444,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"View job","depth":18,"bounds":{"left":0.44791666,"top":0.0,"width":0.038194444,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Code Review","depth":16,"bounds":{"left":0.19166666,"top":0.04388889,"width":0.5375,"height":0.02388889},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Code Review","depth":17,"bounds":{"left":0.19166666,"top":0.044444446,"width":0.07361111,"height":0.023333333},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall the approach is clean — extracting the shared logic into","depth":17,"bounds":{"left":0.19166666,"top":0.08777778,"width":0.28611112,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsCommandTrait","depth":18,"bounds":{"left":0.48125,"top":0.090555556,"width":0.11423611,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is a good call, and the scheduling design (offset cron at","depth":17,"bounds":{"left":0.19166666,"top":0.08777778,"width":0.5083333,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":01","depth":18,"bounds":{"left":0.3454861,"top":0.11388889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to avoid spikes) makes sense. I found two bugs and a few minor points.","depth":17,"bounds":{"left":0.3638889,"top":0.11111111,"width":0.32222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Bugs","depth":16,"bounds":{"left":0.19166666,"top":0.18944444,"width":0.5375,"height":0.024444444},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bugs","depth":17,"bounds":{"left":0.19166666,"top":0.19,"width":0.028819444,"height":0.023333333},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. Null dereference on","depth":18,"bounds":{"left":0.19166666,"top":0.2338889,"width":0.10451389,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig","depth":19,"bounds":{"left":0.29965279,"top":0.23666666,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"bounds":{"left":0.35277778,"top":0.2338889,"width":0.013541667,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:49-51","depth":19,"bounds":{"left":0.36979166,"top":0.23666666,"width":0.20416667,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.29222223,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.20763889,"top":0.29222223,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.25243056,"top":0.29222223,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.2673611,"top":0.29222223,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.27222222,"top":0.29222223,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.2923611,"top":0.29222223,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"bounds":{"left":0.30208334,"top":0.29222223,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.3170139,"top":0.29222223,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"bounds":{"left":0.20277777,"top":0.33055556,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.2125,"top":0.33055556,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.33055556,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.22743055,"top":0.33055556,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.27222222,"top":0.33055556,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"bounds":{"left":0.28229168,"top":0.33055556,"width":0.039930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"bounds":{"left":0.32222223,"top":0.33055556,"width":0.099305555,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"bounds":{"left":0.42152777,"top":0.33055556,"width":0.07986111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"bounds":{"left":0.5013889,"top":0.33055556,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.28166667,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"$team->crm","depth":18,"bounds":{"left":0.19479166,"top":0.3888889,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"can be null if the team has no CRM configuration record. Accessing","depth":17,"bounds":{"left":0.24791667,"top":0.3861111,"width":0.30729166,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->provider","depth":18,"bounds":{"left":0.55833334,"top":0.3888889,"width":0.05,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"on null will throw a fatal error. The existing","depth":17,"bounds":{"left":0.19166666,"top":0.3861111,"width":0.5277778,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":18,"bounds":{"left":0.27881944,"top":0.41222224,"width":0.05451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"job handles this by using","depth":17,"bounds":{"left":0.33680555,"top":0.40944445,"width":0.11666667,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"??","depth":18,"bounds":{"left":0.45694444,"top":0.41222224,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.47013888,"top":0.40944445,"width":0.00625,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig->provider ?? 'unknown'","depth":18,"bounds":{"left":0.4798611,"top":0.41222224,"width":0.16423611,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). The new job needs a null guard here:","depth":17,"bounds":{"left":0.19166666,"top":0.40944445,"width":0.51944447,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.49055555,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.20763889,"top":0.49055555,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.25243056,"top":0.49055555,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.2673611,"top":0.49055555,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.27222222,"top":0.49055555,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.2923611,"top":0.49055555,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"bounds":{"left":0.30208334,"top":0.49055555,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.3170139,"top":0.49055555,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"bounds":{"left":0.20277777,"top":0.5288889,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.2125,"top":0.5288889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.5288889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.22743055,"top":0.5288889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"===","depth":17,"bounds":{"left":0.27222222,"top":0.5288889,"width":0.025,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":17,"bounds":{"left":0.29722223,"top":0.5288889,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"||","depth":17,"bounds":{"left":0.3170139,"top":0.5288889,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.33715278,"top":0.5288889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.3420139,"top":0.5288889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.38680556,"top":0.5288889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"bounds":{"left":0.396875,"top":0.5288889,"width":0.039583333,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"bounds":{"left":0.43645832,"top":0.5288889,"width":0.099652775,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"bounds":{"left":0.5361111,"top":0.5288889,"width":0.07951389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"bounds":{"left":0.20277777,"top":0.5288889,"width":0.42777777,"height":0.035555556},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"logger","depth":17,"bounds":{"left":0.22743055,"top":0.54833335,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.25729167,"top":0.54833335,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"warning","depth":17,"bounds":{"left":0.2673611,"top":0.54833335,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.30208334,"top":0.54833335,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.30729166,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[SyncHubspotObjects] Team has no HubSpot CRM config","depth":17,"bounds":{"left":0.31215277,"top":0.54833335,"width":0.25381944,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.5659722,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", [","depth":17,"bounds":{"left":0.5708333,"top":0.54833335,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.5857639,"top":0.54833335,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.59097224,"top":0.54833335,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.6107639,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=>","depth":17,"bounds":{"left":0.615625,"top":0.54833335,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.6357639,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"bounds":{"left":0.640625,"top":0.54833335,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.66041666,"top":0.54833335,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"bounds":{"left":0.6704861,"top":0.54833335,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]);","depth":17,"bounds":{"left":0.20277777,"top":0.54833335,"width":0.5125,"height":0.035},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"return","depth":17,"bounds":{"left":0.22256945,"top":0.56722224,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";\n}","depth":17,"bounds":{"left":0.20277777,"top":0.56722224,"width":0.05451389,"height":0.035555556},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.48055556,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Fix this →","depth":17,"bounds":{"left":0.19166666,"top":0.6422222,"width":0.043055557,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"bounds":{"left":0.19166666,"top":0.6422222,"width":0.043055557,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.","depth":18,"bounds":{"left":0.19166666,"top":0.7227778,"width":0.011458334,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$lastSyncedAt","depth":19,"bounds":{"left":0.20659722,"top":0.72555554,"width":0.06458333,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is nullable but","depth":18,"bounds":{"left":0.27430555,"top":0.7227778,"width":0.07083333,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":19,"bounds":{"left":0.34861112,"top":0.72555554,"width":0.10451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requires","depth":18,"bounds":{"left":0.45625,"top":0.7227778,"width":0.04375,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":19,"bounds":{"left":0.5034722,"top":0.72555554,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"bounds":{"left":0.5364583,"top":0.7227778,"width":0.013888889,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:85,88,116","depth":19,"bounds":{"left":0.19166666,"top":0.72555554,"width":0.42673612,"height":0.039444443},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.8038889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.20763889,"top":0.8038889,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.2673611,"top":0.8038889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.28229168,"top":0.8038889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.28715277,"top":0.8038889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.33194444,"top":0.8038889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"last_synced_at","depth":17,"bounds":{"left":0.3420139,"top":0.8038889,"width":0.06979167,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.41180557,"top":0.8038889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// can be null on first run","depth":17,"bounds":{"left":0.42673612,"top":0.8038889,"width":0.134375,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.20277777,"top":0.8233333,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.20763889,"top":0.8233333,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.2125,"top":0.8233333,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"bounds":{"left":0.20763889,"top":0.8422222,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.22743055,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":17,"bounds":{"left":0.2375,"top":0.8422222,"width":0.10451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.3420139,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.346875,"top":0.8422222,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"bounds":{"left":0.35208333,"top":0.8422222,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"bounds":{"left":0.4017361,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.41180557,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.41666666,"top":0.8422222,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"bounds":{"left":0.4763889,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// ...","depth":17,"bounds":{"left":0.20277777,"top":0.88055557,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"private","depth":17,"bounds":{"left":0.20277777,"top":0.9,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"function","depth":17,"bounds":{"left":0.24236111,"top":0.9,"width":0.039930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects(","depth":17,"bounds":{"left":0.28229168,"top":0.9,"width":0.114583336,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"HubspotInterface","depth":17,"bounds":{"left":0.396875,"top":0.9,"width":0.07951389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.48125,"top":0.9,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"bounds":{"left":0.48645833,"top":0.9,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"bounds":{"left":0.5361111,"top":0.9,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":17,"bounds":{"left":0.54618055,"top":0.9,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.58090276,"top":0.9,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.5857639,"top":0.9,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"):","depth":17,"bounds":{"left":0.6454861,"top":0.9,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"void","depth":17,"bounds":{"left":0.66041666,"top":0.9,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.79388887,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"With","depth":17,"bounds":{"left":0.19166666,"top":0.95555556,"width":0.023263888,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"declare(strict_types=1)","depth":18,"bounds":{"left":0.21805556,"top":0.9583333,"width":0.114583336,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", passing","depth":17,"bounds":{"left":0.3361111,"top":0.95555556,"width":0.042708334,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":18,"bounds":{"left":0.38229167,"top":0.9583333,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to a","depth":17,"bounds":{"left":0.40555555,"top":0.95555556,"width":0.022222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":18,"bounds":{"left":0.43125,"top":0.9583333,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"-typed parameter throws a","depth":17,"bounds":{"left":0.4642361,"top":0.95555556,"width":0.12256944,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TypeError","depth":18,"bounds":{"left":0.5902778,"top":0.9583333,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". A team that has never synced before will crash on the first run. The parameter should be","depth":17,"bounds":{"left":0.19166666,"top":0.95555556,"width":0.5229167,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"?Carbon","depth":18,"bounds":{"left":0.52118057,"top":0.9816667,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(and","depth":17,"bounds":{"left":0.559375,"top":0.97888887,"width":0.025694445,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncOpportunities","depth":18,"bounds":{"left":0.5885417,"top":0.9816667,"width":0.084375,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"should handle null), or provide a fallback like","depth":17,"bounds":{"left":0.19166666,"top":0.97888887,"width":0.5173611,"height":0.02111113},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon::createFromTimestamp(0)","depth":18,"bounds":{"left":0.36423612,"top":1.0,"width":0.14930555,"height":-0.004999995},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.51666665,"top":1.0,"width":0.0027777778,"height":-0.0022221804},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Fix this →","depth":17,"bounds":{"left":0.19166666,"top":1.0,"width":0.043055557,"height":-0.043333292},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"bounds":{"left":0.19166666,"top":1.0,"width":0.043055557,"height":-0.043333292},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Minor Issues","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Minor Issues","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. Double call to","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix()","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"prefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() ?","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() .","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"''","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"getLogPrefix()","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is called twice. Trivial but unnecessary — capture the result in a variable first.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"command doesn't guard HubSpot when","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{team}","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"argument is given —","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/SyncObjects.php:40-41","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teams","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[] = Team::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"idOrUuId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-4785588748818184246
|
8380486226885671373
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);...
|
NULL
|
|
61010
|
1315
|
50
|
2026-04-21T06:31:02.850946+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-21/1776 /Users/lukas/.screenpipe/data/data/2026-04-21/1776753062850_m2.jpg...
|
Firefox
|
Jira — Work
|
1
|
jiminny.atlassian.net/jira/servicedesk/projects/SR jiminny.atlassian.net/jira/servicedesk/projects/SRD/queues/custom/37/SRD-6793...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
Jira
Jira
Close tab
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Transferring data from jiminny.atlassian.net…...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0018284575,"top":0.0518755,"width":0.07596409,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Jira","depth":4,"bounds":{"left":0.0,"top":0.09497207,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Jira","depth":5,"bounds":{"left":0.013297873,"top":0.10614525,"width":0.0063164895,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.10215483,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"bounds":{"left":0.0,"top":0.12769353,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.13886672,"width":0.08344415,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.16041501,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.17158818,"width":0.19963431,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny MCP Connector - Product - Confluence","depth":4,"bounds":{"left":0.0,"top":0.19313647,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny MCP Connector - Product - Confluence","depth":5,"bounds":{"left":0.013297873,"top":0.20430966,"width":0.08294548,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":4,"bounds":{"left":0.0,"top":0.22585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.23703113,"width":0.15791224,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny Mail","depth":4,"bounds":{"left":0.0,"top":0.2585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny Mail","depth":5,"bounds":{"left":0.013297873,"top":0.2697526,"width":0.02144282,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":4,"bounds":{"left":0.0,"top":0.29130086,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.30247405,"width":0.08610372,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Feed — jiminny — Sentry","depth":4,"bounds":{"left":0.0,"top":0.32402235,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Feed — jiminny — Sentry","depth":5,"bounds":{"left":0.013297873,"top":0.33519554,"width":0.042719416,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.0,"top":0.3567438,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.013297873,"top":0.367917,"width":0.013131649,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.38946527,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.40063846,"width":0.1740359,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.42218676,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.43335995,"width":0.039228722,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.45650437,"width":0.07413564,"height":0.025538707},"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},"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},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Close bookmarks (⌘B)","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Bookmarks","depth":5,"bounds":{"left":0.083277926,"top":0.06943336,"width":0.026761968,"height":0.014764565},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bookmarks","depth":6,"bounds":{"left":0.083277926,"top":0.06943336,"width":0.026761968,"height":0.014764565},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close sidebar","depth":6,"bounds":{"left":0.16938165,"top":0.06424581,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXTextField","text":"Search bookmarks","depth":7,"bounds":{"left":0.082446806,"top":0.09976058,"width":0.09857048,"height":0.025538707},"help_text":"","role_description":"search text field","subrole":"AXSearchField","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Transferring data from jiminny.atlassian.net…","depth":5,"bounds":{"left":0.18783244,"top":0.9876297,"width":0.078125,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
714183906417343914
|
8380485882203796969
|
visual_change
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
Jira
Jira
Close tab
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Transferring data from jiminny.atlassian.net…...
|
61009
|
|
60914
|
1313
|
40
|
2026-04-21T06:25:05.642163+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-21/1776 /Users/lukas/.screenpipe/data/data/2026-04-21/1776752705642_m2.jpg...
|
Firefox
|
JY-20701 | Reschedule HubSpot Sync Objects by yalo JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11989
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (
60 * 60 * 6
). Appropriate given the more frequent, presumably lighter runs.
Stagger delay
: HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.
Add or remove reactions
@yalokin-jiminny
yalokin-jiminny
yalokin-jiminny
force-pushed
force-pushed
the
JY-20553-debug-crm-sync-delays
branch from
f6c0ec3
f6c0ec3
to
5928f60
5928f60
Compare
Compare
yesterday...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0018284575,"top":0.0518755,"width":0.07596409,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"bounds":{"left":0.0,"top":0.09497207,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.10614525,"width":0.08344415,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"bounds":{"left":0.0,"top":0.12769353,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.13886672,"width":0.08344415,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.16041501,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.17158818,"width":0.19963431,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny MCP Connector - Product - Confluence","depth":4,"bounds":{"left":0.0,"top":0.19313647,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny MCP Connector - Product - Confluence","depth":5,"bounds":{"left":0.013297873,"top":0.20430966,"width":0.08294548,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":4,"bounds":{"left":0.0,"top":0.22585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.23703113,"width":0.15791224,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny Mail","depth":4,"bounds":{"left":0.0,"top":0.2585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny Mail","depth":5,"bounds":{"left":0.013297873,"top":0.2697526,"width":0.02144282,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":4,"bounds":{"left":0.0,"top":0.29130086,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.30247405,"width":0.08610372,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Feed — jiminny — Sentry","depth":4,"bounds":{"left":0.0,"top":0.32402235,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Feed — jiminny — Sentry","depth":5,"bounds":{"left":0.013297873,"top":0.33519554,"width":0.042719416,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.0,"top":0.3567438,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.013297873,"top":0.367917,"width":0.013131649,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.38946527,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.40063846,"width":0.1740359,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.39664805,"width":0.007978723,"height":0.01915403},"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.4237829,"width":0.07413564,"height":0.025538707},"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},"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},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"bounds":{"left":0.07962101,"top":0.0518755,"width":0.0003324468,"height":0.0007980846},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"bounds":{"left":0.07962101,"top":0.05347167,"width":0.0029920214,"height":0.21468475},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"bounds":{"left":0.08494016,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"bounds":{"left":0.099567816,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"bounds":{"left":0.112865694,"top":0.06464485,"width":0.018949468,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"bounds":{"left":0.11486037,"top":0.07063048,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"bounds":{"left":0.13680187,"top":0.06464485,"width":0.017785905,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"bounds":{"left":0.13879654,"top":0.07063048,"width":0.008477394,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"bounds":{"left":0.81698805,"top":0.06464485,"width":0.06565824,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"bounds":{"left":0.82928854,"top":0.07063048,"width":0.011801862,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"bounds":{"left":0.8424202,"top":0.07222666,"width":0.002493351,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"bounds":{"left":0.84640956,"top":0.07063048,"width":0.021276595,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"bounds":{"left":0.88464093,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"bounds":{"left":0.8949468,"top":0.06464485,"width":0.008643617,"height":0.025538707},"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"bounds":{"left":0.9115692,"top":0.06464485,"width":0.01662234,"height":0.025538707},"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"bounds":{"left":0.93085104,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"bounds":{"left":0.94414896,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"bounds":{"left":0.9574468,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"bounds":{"left":0.97074467,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"bounds":{"left":0.9840425,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"bounds":{"left":0.079288565,"top":0.051077414,"width":0.0003324468,"height":0.0007980846},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"bounds":{"left":0.079288565,"top":0.05387071,"width":0.0787899,"height":0.023144454},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"bounds":{"left":0.08494016,"top":0.09936153,"width":0.025099734,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"bounds":{"left":0.095744684,"top":0.10574621,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (31)","depth":12,"bounds":{"left":0.11269947,"top":0.09936153,"width":0.054521278,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"bounds":{"left":0.12333777,"top":0.10574621,"width":0.02925532,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"bounds":{"left":0.15525267,"top":0.113727055,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"31","depth":14,"bounds":{"left":0.15824468,"top":0.113727055,"width":0.004986702,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"bounds":{"left":0.16323139,"top":0.113727055,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"bounds":{"left":0.16988032,"top":0.09936153,"width":0.029089095,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"bounds":{"left":0.18085106,"top":0.10574621,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"bounds":{"left":0.20162898,"top":0.09936153,"width":0.03025266,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"bounds":{"left":0.21276596,"top":0.10574621,"width":0.015957447,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"bounds":{"left":0.23454122,"top":0.09936153,"width":0.022938829,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"bounds":{"left":0.24534574,"top":0.10574621,"width":0.009142287,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (21)","depth":12,"bounds":{"left":0.2601396,"top":0.09936153,"width":0.069980055,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"bounds":{"left":0.27194148,"top":0.10574621,"width":0.04255319,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"bounds":{"left":0.31831783,"top":0.113727055,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"21","depth":14,"bounds":{"left":0.32130983,"top":0.113727055,"width":0.0048204786,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"bounds":{"left":0.32613033,"top":0.113727055,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"bounds":{"left":0.33277926,"top":0.09936153,"width":0.03125,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"bounds":{"left":0.34391624,"top":0.10574621,"width":0.016788565,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"bounds":{"left":0.36668882,"top":0.09936153,"width":0.032081116,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"bounds":{"left":0.3778258,"top":0.10574621,"width":0.01761968,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"bounds":{"left":0.09325133,"top":0.14365523,"width":0.0003324468,"height":0.016759777},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"bounds":{"left":0.09325133,"top":0.1452514,"width":0.039228722,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"bounds":{"left":0.09325133,"top":0.1452514,"width":0.2159242,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"bounds":{"left":0.30917552,"top":0.1452514,"width":0.04055851,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"bounds":{"left":0.30917552,"top":0.1452514,"width":0.04055851,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"bounds":{"left":0.34973404,"top":0.1452514,"width":0.08261303,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"bounds":{"left":0.4323471,"top":0.1452514,"width":0.05219415,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"bounds":{"left":0.4323471,"top":0.1452514,"width":0.05219415,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"bounds":{"left":0.48454124,"top":0.1452514,"width":0.0013297872,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"bounds":{"left":0.98636967,"top":0.13886672,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Review requested","depth":15,"bounds":{"left":0.35006648,"top":0.2019154,"width":0.0003324468,"height":0.016759777},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Review requested","depth":16,"bounds":{"left":0.35006648,"top":0.20351157,"width":0.039893616,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"bounds":{"left":0.35006648,"top":0.20351157,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"bounds":{"left":0.35006648,"top":0.20351157,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requested your review on this pull request.","depth":15,"bounds":{"left":0.3854721,"top":0.20351157,"width":0.09158909,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add your review","depth":14,"bounds":{"left":0.70212764,"top":0.19872306,"width":0.036901597,"height":0.022346368},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add your review","depth":16,"bounds":{"left":0.70511967,"top":0.20391062,"width":0.030917553,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title","depth":13,"bounds":{"left":0.33776596,"top":0.24221867,"width":0.26230052,"height":0.031923383},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects","depth":14,"bounds":{"left":0.33776596,"top":0.24301676,"width":0.2122673,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"bounds":{"left":0.55269283,"top":0.24301676,"width":0.006482713,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11989","depth":15,"bounds":{"left":0.55917555,"top":0.24301676,"width":0.028922873,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"bounds":{"left":0.5894282,"top":0.24541101,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Awaiting approval","depth":13,"bounds":{"left":0.6569149,"top":0.24860336,"width":0.055518616,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Awaiting approval","depth":15,"bounds":{"left":0.66921544,"top":0.254589,"width":0.038896278,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"bounds":{"left":0.7137633,"top":0.24860336,"width":0.02825798,"height":0.025538707},"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"bounds":{"left":0.7180851,"top":0.254589,"width":0.011635638,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"bounds":{"left":0.34840426,"top":0.28651237,"width":0.011968086,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"bounds":{"left":0.36702126,"top":0.28332004,"width":0.034075797,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"bounds":{"left":0.36702126,"top":0.2849162,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 22 commits into","depth":15,"bounds":{"left":0.40242687,"top":0.2849162,"width":0.069148935,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"bounds":{"left":0.47290558,"top":0.282921,"width":0.018284574,"height":0.017557861},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"bounds":{"left":0.47490028,"top":0.28611332,"width":0.014295213,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"bounds":{"left":0.49251994,"top":0.2849162,"width":0.009973404,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701-reschedule-HubSpot-processing","depth":16,"bounds":{"left":0.50382316,"top":0.282921,"width":0.09524601,"height":0.017557861},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701-reschedule-HubSpot-processing","depth":17,"bounds":{"left":0.50581783,"top":0.28611332,"width":0.09125665,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"bounds":{"left":0.60039896,"top":0.28052673,"width":0.00930851,"height":0.022346368},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 949 additions & 97 deletions","depth":14,"bounds":{"left":0.7067819,"top":0.3367917,"width":0.019946808,"height":0.11412609},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (5)","depth":16,"bounds":{"left":0.33776596,"top":0.31883478,"width":0.057347074,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"bounds":{"left":0.35139626,"top":0.32841182,"width":0.028091755,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.38946143,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5","depth":18,"bounds":{"left":0.39245346,"top":0.32841182,"width":0.0028257978,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.39527926,"top":0.32841182,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (22)","depth":16,"bounds":{"left":0.39511302,"top":0.31883478,"width":0.05069814,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"bounds":{"left":0.40874335,"top":0.32841182,"width":0.019115692,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.4401596,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22","depth":18,"bounds":{"left":0.4431516,"top":0.32841182,"width":0.005485372,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.44863698,"top":0.32841182,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (3)","depth":16,"bounds":{"left":0.44581118,"top":0.31883478,"width":0.04504654,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"bounds":{"left":0.45944148,"top":0.32841182,"width":0.015957447,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.48520613,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"bounds":{"left":0.48819813,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.49119017,"top":0.32841182,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (11)","depth":16,"bounds":{"left":0.49085772,"top":0.31883478,"width":0.060339097,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"bounds":{"left":0.50448805,"top":0.32841182,"width":0.029753989,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.5455452,"top":0.32841182,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11","depth":18,"bounds":{"left":0.54853725,"top":0.32841182,"width":0.0043218085,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.55285907,"top":0.32841182,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Conversation","depth":12,"bounds":{"left":0.33776596,"top":0.3643256,"width":0.0003324468,"height":0.0007980846},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"bounds":{"left":0.33776596,"top":0.36711892,"width":0.048204787,"height":0.023144454},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":12,"bounds":{"left":0.33776596,"top":0.3643256,"width":0.013297873,"height":0.031923383},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"bounds":{"left":0.61136967,"top":0.3651237,"width":0.007978723,"height":0.02952913},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago •","depth":14,"bounds":{"left":0.3620346,"top":0.3651237,"width":0.24135639,"height":0.02952913},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":16,"bounds":{"left":0.3620346,"top":0.37310454,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":17,"bounds":{"left":0.3620346,"top":0.37310454,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"bounds":{"left":0.39744017,"top":0.37310454,"width":0.025598405,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":15,"bounds":{"left":0.42436835,"top":0.3715084,"width":0.023603724,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":17,"bounds":{"left":0.42436835,"top":0.37310454,"width":0.023603724,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"bounds":{"left":0.44930187,"top":0.37310454,"width":0.0021609042,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"bounds":{"left":0.45262632,"top":0.3715084,"width":0.020279255,"height":0.016759777},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"bounds":{"left":0.45262632,"top":0.37310454,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JIRA: JY-20701","depth":16,"bounds":{"left":0.3620346,"top":0.40822026,"width":0.25731382,"height":0.017557861},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JIRA:","depth":17,"bounds":{"left":0.3620346,"top":0.4086193,"width":0.015791224,"height":0.016759777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701","depth":17,"bounds":{"left":0.3778258,"top":0.4086193,"width":0.026263298,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701","depth":18,"bounds":{"left":0.3778258,"top":0.4086193,"width":0.026263298,"height":0.016759777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Casus:","depth":16,"bounds":{"left":0.3620346,"top":0.44493216,"width":0.25731382,"height":0.01396648},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Casus:","depth":17,"bounds":{"left":0.3620346,"top":0.44493216,"width":0.015292553,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"We want more fluent curve of the jobs distribution (no spikes) in","depth":18,"bounds":{"left":0.3700133,"top":0.47326416,"width":0.13879654,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm-sync","depth":19,"bounds":{"left":0.51047206,"top":0.47525936,"width":0.018949468,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"queue","depth":18,"bounds":{"left":0.53108376,"top":0.47326416,"width":0.01462766,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times","depth":18,"bounds":{"left":0.3700133,"top":0.49281725,"width":0.24750665,"height":0.030726258},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Changes:","depth":16,"bounds":{"left":0.3620346,"top":0.5442937,"width":0.25731382,"height":0.01396648},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changes:","depth":17,"bounds":{"left":0.3620346,"top":0.5442937,"width":0.021110373,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Remove HubSpot from SyncObjects","depth":18,"bounds":{"left":0.3700133,"top":0.5726257,"width":0.07712766,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Create separate HubSpotSyncObjects that runs each 5 min instead of each 30","depth":18,"bounds":{"left":0.3700133,"top":0.59217876,"width":0.16855054,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Extract common code","depth":18,"bounds":{"left":0.3700133,"top":0.6121309,"width":0.047041222,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Improve processing Webhooks jobs distribution","depth":18,"bounds":{"left":0.3700133,"top":0.63168395,"width":0.101894945,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Optimize ImportOpportunityBatch:","depth":18,"bounds":{"left":0.3700133,"top":0.65163606,"width":0.07396942,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cache OpportunitySyncableFields, OwnerProfiles, RecordType","depth":20,"bounds":{"left":0.37799203,"top":0.6683959,"width":0.13347739,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"remove findContactByConfigurationAndId - Contact ID already exist","depth":20,"bounds":{"left":0.37799203,"top":0.68834794,"width":0.1456117,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"add timings log","depth":20,"bounds":{"left":0.37799203,"top":0.70790106,"width":0.032912236,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":16,"bounds":{"left":0.3620346,"top":0.73623306,"width":0.008643617,"height":0.0207502},"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":13,"bounds":{"left":0.33776596,"top":0.7960894,"width":0.013297873,"height":0.031923383},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"bounds":{"left":0.61136967,"top":0.79688746,"width":0.007978723,"height":0.02952913},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Author","depth":15,"bounds":{"left":0.5934175,"top":0.80606544,"width":0.012965426,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago","depth":13,"bounds":{"left":0.3620346,"top":0.79688746,"width":0.22240691,"height":0.02952913},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"bounds":{"left":0.3620346,"top":0.80486834,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"bounds":{"left":0.3620346,"top":0.80486834,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"bounds":{"left":0.39744017,"top":0.80486834,"width":0.025598405,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"bounds":{"left":0.42436835,"top":0.8032721,"width":0.023603724,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"bounds":{"left":0.42436835,"top":0.80486834,"width":0.023603724,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":17,"bounds":{"left":0.3620346,"top":0.8415802,"width":0.019115692,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@claude","depth":18,"bounds":{"left":0.3620346,"top":0.8415802,"width":0.019115692,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"bounds":{"left":0.3620346,"top":0.86951315,"width":0.008643617,"height":0.0207502},"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"react with eyes","depth":14,"bounds":{"left":0.37200797,"top":0.86951315,"width":0.013796543,"height":0.0207502},"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"👀","depth":16,"bounds":{"left":0.37416887,"top":0.8747007,"width":0.004155585,"height":0.012769354},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":16,"bounds":{"left":0.38098404,"top":0.8747007,"width":0.0018284575,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":13,"bounds":{"left":0.33776596,"top":0.9293695,"width":0.013297873,"height":0.031923383},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"bounds":{"left":0.61136967,"top":0.9301676,"width":0.007978723,"height":0.02952913},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"claude bot commented 4 days ago •","depth":13,"bounds":{"left":0.3620346,"top":0.9301676,"width":0.24135639,"height":0.029928172},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"claude","depth":15,"bounds":{"left":0.3620346,"top":0.93814844,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"claude","depth":16,"bounds":{"left":0.3620346,"top":0.93814844,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":16,"bounds":{"left":0.3804854,"top":0.9397446,"width":0.006482713,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"bounds":{"left":0.390625,"top":0.93814844,"width":0.02543218,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"bounds":{"left":0.41738698,"top":0.9365523,"width":0.023603724,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"bounds":{"left":0.41738698,"top":0.93814844,"width":0.023603724,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":16,"bounds":{"left":0.44232047,"top":0.93814844,"width":0.0021609042,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":16,"bounds":{"left":0.44581118,"top":0.9365523,"width":0.020113032,"height":0.016759777},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":18,"bounds":{"left":0.44581118,"top":0.93814844,"width":0.014793883,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Claude finished","depth":18,"bounds":{"left":0.3620346,"top":0.97525936,"width":0.036070477,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":18,"bounds":{"left":0.39810506,"top":0.97525936,"width":0.038397606,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@yalokin-jiminny","depth":19,"bounds":{"left":0.39810506,"top":0.97525936,"width":0.038397606,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s task in 1m 59s","depth":18,"bounds":{"left":0.43650267,"top":0.97525936,"width":0.03756649,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"——","depth":17,"bounds":{"left":0.47406915,"top":0.97525936,"width":0.010638298,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"View job","depth":17,"bounds":{"left":0.48470744,"top":0.97525936,"width":0.018284574,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"View job","depth":18,"bounds":{"left":0.48470744,"top":0.97525936,"width":0.018284574,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Code Review","depth":16,"bounds":{"left":0.3620346,"top":1.0,"width":0.25731382,"height":-0.0315243},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Code Review","depth":17,"bounds":{"left":0.3620346,"top":1.0,"width":0.03523936,"height":-0.031923413},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall the approach is clean — extracting the shared logic into","depth":17,"bounds":{"left":0.3620346,"top":1.0,"width":0.13696809,"height":-0.06304872},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsCommandTrait","depth":18,"bounds":{"left":0.5006649,"top":1.0,"width":0.0546875,"height":-0.065043926},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is a good call, and the scheduling design (offset cron at","depth":17,"bounds":{"left":0.3620346,"top":1.0,"width":0.24335106,"height":-0.06304872},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":01","depth":18,"bounds":{"left":0.43567154,"top":1.0,"width":0.0071476065,"height":-0.08180368},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to avoid spikes) makes sense. I found two bugs and a few minor points.","depth":17,"bounds":{"left":0.44448137,"top":1.0,"width":0.15425532,"height":-0.07980847},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Bugs","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bugs","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. Null dereference on","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:49-51","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"$team->crm","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"can be null if the team has no CRM configuration record. Accessing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->provider","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"on null will throw a fatal error. The existing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"job handles this by using","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"??","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig->provider ?? 'unknown'","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). The new job needs a null guard here:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"===","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"||","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"logger","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"warning","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[SyncHubspotObjects] Team has no HubSpot CRM config","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", [","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=>","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]);","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"return","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";\n}","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Fix this →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$lastSyncedAt","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is nullable but","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requires","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:85,88,116","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"last_synced_at","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// can be null on first run","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// ...","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"private","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"function","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"HubspotInterface","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"):","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"void","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"With","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"declare(strict_types=1)","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", passing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"-typed parameter throws a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TypeError","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". A team that has never synced before will crash on the first run. The parameter should be","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"?Carbon","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(and","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncOpportunities","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"should handle null), or provide a fallback like","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon::createFromTimestamp(0)","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Fix this →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Minor Issues","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Minor Issues","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. Double call to","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix()","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"prefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() ?","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() .","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"''","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"getLogPrefix()","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is called twice. Trivial but unnecessary — capture the result in a variable first.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"command doesn't guard HubSpot when","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{team}","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"argument is given —","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/SyncObjects.php:40-41","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teams","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[] = Team::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"idOrUuId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// no provider check","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Running","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-objects <hubspot-team-id>","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dispatches a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsJob","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Design Notes","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Design Notes","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Scheduler placement","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is placed inside","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"scheduleEveryFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"but uses a manual cron","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"instead of","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->everyFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SQS visibility timeout","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncHubspotObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"uses 1 hour (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"60 * 60","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") vs","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s 6 hours (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"60 * 60 * 6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). Appropriate given the more frequent, presumably lighter runs.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Stagger delay","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"yalokin-jiminny","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"force-pushed","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"force-pushed","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"the","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20553-debug-crm-sync-delays","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"branch from","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"f6c0ec3","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"f6c0ec3","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"5928f60","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5928f60","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Compare","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Compare","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yesterday","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-5062705929174697826
|
8380481863211482445
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (
60 * 60 * 6
). Appropriate given the more frequent, presumably lighter runs.
Stagger delay
: HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.
Add or remove reactions
@yalokin-jiminny
yalokin-jiminny
yalokin-jiminny
force-pushed
force-pushed
the
JY-20553-debug-crm-sync-delays
branch from
f6c0ec3
f6c0ec3
to
5928f60
5928f60
Compare
Compare
yesterday...
|
NULL
|
|
60915
|
1312
|
28
|
2026-04-21T06:25:18.735655+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-21/1776 /Users/lukas/.screenpipe/data/data/2026-04-21/1776752718735_m1.jpg...
|
Firefox
|
JY-20701 | Reschedule HubSpot Sync Objects by yalo JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11989
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (
60 * 60 * 6
). Appropriate given the more frequent, presumably lighter runs.
Stagger delay
: HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.
Add or remove reactions
@yalokin-jiminny
yalokin-jiminny
yalokin-jiminny
force-pushed
force-pushed
the
JY-20553-debug-crm-sync-delays
branch from
f6c0ec3
f6c0ec3
to
5928f60...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny MCP Connector - Product - Confluence","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny MCP Connector - Product - Confluence","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny Mail","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny Mail","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Feed — jiminny — Sentry","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Feed — jiminny — Sentry","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"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,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (31)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"31","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (21)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"21","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Review requested","depth":15,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Review requested","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requested your review on this pull request.","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add your review","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add your review","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11989","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Awaiting approval","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Awaiting approval","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 22 commits into","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701-reschedule-HubSpot-processing","depth":16,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701-reschedule-HubSpot-processing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 949 additions & 97 deletions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (5)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (22)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (11)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Conversation","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago •","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JIRA: JY-20701","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JIRA:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Casus:","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Casus:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"We want more fluent curve of the jobs distribution (no spikes) in","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm-sync","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"queue","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Changes:","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changes:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Remove HubSpot from SyncObjects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Create separate HubSpotSyncObjects that runs each 5 min instead of each 30","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Extract common code","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Improve processing Webhooks jobs distribution","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Optimize ImportOpportunityBatch:","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cache OpportunitySyncableFields, OwnerProfiles, RecordType","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"remove findContactByConfigurationAndId - Contact ID already exist","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"add timings log","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":16,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":13,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Author","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@claude","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"react with eyes","depth":14,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"👀","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":13,"bounds":{"left":0.14097223,"top":0.0,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"bounds":{"left":0.7125,"top":0.0,"width":0.016666668,"height":0.04111111},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"claude bot commented 4 days ago •","depth":13,"bounds":{"left":0.19166666,"top":0.0,"width":0.50416666,"height":0.041666668},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"claude","depth":15,"bounds":{"left":0.19166666,"top":0.0,"width":0.03125,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"claude","depth":16,"bounds":{"left":0.19166666,"top":0.0,"width":0.03125,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":16,"bounds":{"left":0.23020834,"top":0.0,"width":0.013541667,"height":0.016666668},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"bounds":{"left":0.25138888,"top":0.0,"width":0.053125,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"bounds":{"left":0.30729166,"top":0.0,"width":0.049305554,"height":0.023333333},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"bounds":{"left":0.30729166,"top":0.0,"width":0.049305554,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":16,"bounds":{"left":0.359375,"top":0.0,"width":0.004513889,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":16,"bounds":{"left":0.36666667,"top":0.0,"width":0.042013887,"height":0.023333333},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":18,"bounds":{"left":0.36666667,"top":0.0,"width":0.030902777,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Claude finished","depth":18,"bounds":{"left":0.19166666,"top":0.0,"width":0.07534722,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":18,"bounds":{"left":0.26701388,"top":0.0,"width":0.08020833,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@yalokin-jiminny","depth":19,"bounds":{"left":0.26701388,"top":0.0,"width":0.08020833,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s task in 1m 59s","depth":18,"bounds":{"left":0.3472222,"top":0.0,"width":0.07847222,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"——","depth":17,"bounds":{"left":0.42569444,"top":0.0,"width":0.022222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"View job","depth":17,"bounds":{"left":0.44791666,"top":0.0,"width":0.038194444,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"View job","depth":18,"bounds":{"left":0.44791666,"top":0.0,"width":0.038194444,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Code Review","depth":16,"bounds":{"left":0.19166666,"top":0.04388889,"width":0.5375,"height":0.02388889},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Code Review","depth":17,"bounds":{"left":0.19166666,"top":0.044444446,"width":0.07361111,"height":0.023333333},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall the approach is clean — extracting the shared logic into","depth":17,"bounds":{"left":0.19166666,"top":0.08777778,"width":0.28611112,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsCommandTrait","depth":18,"bounds":{"left":0.48125,"top":0.090555556,"width":0.11423611,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is a good call, and the scheduling design (offset cron at","depth":17,"bounds":{"left":0.19166666,"top":0.08777778,"width":0.5083333,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":01","depth":18,"bounds":{"left":0.3454861,"top":0.11388889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to avoid spikes) makes sense. I found two bugs and a few minor points.","depth":17,"bounds":{"left":0.3638889,"top":0.11111111,"width":0.32222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Bugs","depth":16,"bounds":{"left":0.19166666,"top":0.18944444,"width":0.5375,"height":0.024444444},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bugs","depth":17,"bounds":{"left":0.19166666,"top":0.19,"width":0.028819444,"height":0.023333333},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. Null dereference on","depth":18,"bounds":{"left":0.19166666,"top":0.2338889,"width":0.10451389,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig","depth":19,"bounds":{"left":0.29965279,"top":0.23666666,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"bounds":{"left":0.35277778,"top":0.2338889,"width":0.013541667,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:49-51","depth":19,"bounds":{"left":0.36979166,"top":0.23666666,"width":0.20416667,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.29222223,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.20763889,"top":0.29222223,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.25243056,"top":0.29222223,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.2673611,"top":0.29222223,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.27222222,"top":0.29222223,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.2923611,"top":0.29222223,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"bounds":{"left":0.30208334,"top":0.29222223,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.3170139,"top":0.29222223,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"bounds":{"left":0.20277777,"top":0.33055556,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.2125,"top":0.33055556,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.33055556,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.22743055,"top":0.33055556,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.27222222,"top":0.33055556,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"bounds":{"left":0.28229168,"top":0.33055556,"width":0.039930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"bounds":{"left":0.32222223,"top":0.33055556,"width":0.099305555,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"bounds":{"left":0.42152777,"top":0.33055556,"width":0.07986111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"bounds":{"left":0.5013889,"top":0.33055556,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.28166667,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"$team->crm","depth":18,"bounds":{"left":0.19479166,"top":0.3888889,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"can be null if the team has no CRM configuration record. Accessing","depth":17,"bounds":{"left":0.24791667,"top":0.3861111,"width":0.30729166,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->provider","depth":18,"bounds":{"left":0.55833334,"top":0.3888889,"width":0.05,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"on null will throw a fatal error. The existing","depth":17,"bounds":{"left":0.19166666,"top":0.3861111,"width":0.5277778,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":18,"bounds":{"left":0.27881944,"top":0.41222224,"width":0.05451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"job handles this by using","depth":17,"bounds":{"left":0.33680555,"top":0.40944445,"width":0.11666667,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"??","depth":18,"bounds":{"left":0.45694444,"top":0.41222224,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.47013888,"top":0.40944445,"width":0.00625,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig->provider ?? 'unknown'","depth":18,"bounds":{"left":0.4798611,"top":0.41222224,"width":0.16423611,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). The new job needs a null guard here:","depth":17,"bounds":{"left":0.19166666,"top":0.40944445,"width":0.51944447,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.49055555,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.20763889,"top":0.49055555,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.25243056,"top":0.49055555,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.2673611,"top":0.49055555,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.27222222,"top":0.49055555,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.2923611,"top":0.49055555,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"bounds":{"left":0.30208334,"top":0.49055555,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.3170139,"top":0.49055555,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"bounds":{"left":0.20277777,"top":0.5288889,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.2125,"top":0.5288889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.5288889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.22743055,"top":0.5288889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"===","depth":17,"bounds":{"left":0.27222222,"top":0.5288889,"width":0.025,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":17,"bounds":{"left":0.29722223,"top":0.5288889,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"||","depth":17,"bounds":{"left":0.3170139,"top":0.5288889,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.33715278,"top":0.5288889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.3420139,"top":0.5288889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.38680556,"top":0.5288889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"bounds":{"left":0.396875,"top":0.5288889,"width":0.039583333,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"bounds":{"left":0.43645832,"top":0.5288889,"width":0.099652775,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"bounds":{"left":0.5361111,"top":0.5288889,"width":0.07951389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"bounds":{"left":0.20277777,"top":0.5288889,"width":0.42777777,"height":0.035555556},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"logger","depth":17,"bounds":{"left":0.22743055,"top":0.54833335,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.25729167,"top":0.54833335,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"warning","depth":17,"bounds":{"left":0.2673611,"top":0.54833335,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.30208334,"top":0.54833335,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.30729166,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[SyncHubspotObjects] Team has no HubSpot CRM config","depth":17,"bounds":{"left":0.31215277,"top":0.54833335,"width":0.25381944,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.5659722,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", [","depth":17,"bounds":{"left":0.5708333,"top":0.54833335,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.5857639,"top":0.54833335,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.59097224,"top":0.54833335,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.6107639,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=>","depth":17,"bounds":{"left":0.615625,"top":0.54833335,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.6357639,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"bounds":{"left":0.640625,"top":0.54833335,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.66041666,"top":0.54833335,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"bounds":{"left":0.6704861,"top":0.54833335,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]);","depth":17,"bounds":{"left":0.20277777,"top":0.54833335,"width":0.5125,"height":0.035},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"return","depth":17,"bounds":{"left":0.22256945,"top":0.56722224,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";\n}","depth":17,"bounds":{"left":0.20277777,"top":0.56722224,"width":0.05451389,"height":0.035555556},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.48055556,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Fix this →","depth":17,"bounds":{"left":0.19166666,"top":0.6422222,"width":0.043055557,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"bounds":{"left":0.19166666,"top":0.6422222,"width":0.043055557,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.","depth":18,"bounds":{"left":0.19166666,"top":0.7227778,"width":0.011458334,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$lastSyncedAt","depth":19,"bounds":{"left":0.20659722,"top":0.72555554,"width":0.06458333,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is nullable but","depth":18,"bounds":{"left":0.27430555,"top":0.7227778,"width":0.07083333,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":19,"bounds":{"left":0.34861112,"top":0.72555554,"width":0.10451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requires","depth":18,"bounds":{"left":0.45625,"top":0.7227778,"width":0.04375,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":19,"bounds":{"left":0.5034722,"top":0.72555554,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"bounds":{"left":0.5364583,"top":0.7227778,"width":0.013888889,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:85,88,116","depth":19,"bounds":{"left":0.19166666,"top":0.72555554,"width":0.42673612,"height":0.039444443},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.8038889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.20763889,"top":0.8038889,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.2673611,"top":0.8038889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.28229168,"top":0.8038889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.28715277,"top":0.8038889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.33194444,"top":0.8038889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"last_synced_at","depth":17,"bounds":{"left":0.3420139,"top":0.8038889,"width":0.06979167,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.41180557,"top":0.8038889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// can be null on first run","depth":17,"bounds":{"left":0.42673612,"top":0.8038889,"width":0.134375,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.20277777,"top":0.8233333,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.20763889,"top":0.8233333,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.2125,"top":0.8233333,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"bounds":{"left":0.20763889,"top":0.8422222,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.22743055,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":17,"bounds":{"left":0.2375,"top":0.8422222,"width":0.10451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.3420139,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.346875,"top":0.8422222,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"bounds":{"left":0.35208333,"top":0.8422222,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"bounds":{"left":0.4017361,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.41180557,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.41666666,"top":0.8422222,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"bounds":{"left":0.4763889,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// ...","depth":17,"bounds":{"left":0.20277777,"top":0.88055557,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"private","depth":17,"bounds":{"left":0.20277777,"top":0.9,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"function","depth":17,"bounds":{"left":0.24236111,"top":0.9,"width":0.039930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects(","depth":17,"bounds":{"left":0.28229168,"top":0.9,"width":0.114583336,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"HubspotInterface","depth":17,"bounds":{"left":0.396875,"top":0.9,"width":0.07951389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.48125,"top":0.9,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"bounds":{"left":0.48645833,"top":0.9,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"bounds":{"left":0.5361111,"top":0.9,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":17,"bounds":{"left":0.54618055,"top":0.9,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.58090276,"top":0.9,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.5857639,"top":0.9,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"):","depth":17,"bounds":{"left":0.6454861,"top":0.9,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"void","depth":17,"bounds":{"left":0.66041666,"top":0.9,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.79388887,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"With","depth":17,"bounds":{"left":0.19166666,"top":0.95555556,"width":0.023263888,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"declare(strict_types=1)","depth":18,"bounds":{"left":0.21805556,"top":0.9583333,"width":0.114583336,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", passing","depth":17,"bounds":{"left":0.3361111,"top":0.95555556,"width":0.042708334,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":18,"bounds":{"left":0.38229167,"top":0.9583333,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to a","depth":17,"bounds":{"left":0.40555555,"top":0.95555556,"width":0.022222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":18,"bounds":{"left":0.43125,"top":0.9583333,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"-typed parameter throws a","depth":17,"bounds":{"left":0.4642361,"top":0.95555556,"width":0.12256944,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TypeError","depth":18,"bounds":{"left":0.5902778,"top":0.9583333,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". A team that has never synced before will crash on the first run. The parameter should be","depth":17,"bounds":{"left":0.19166666,"top":0.95555556,"width":0.5229167,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"?Carbon","depth":18,"bounds":{"left":0.52118057,"top":0.9816667,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(and","depth":17,"bounds":{"left":0.559375,"top":0.97888887,"width":0.025694445,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncOpportunities","depth":18,"bounds":{"left":0.5885417,"top":0.9816667,"width":0.084375,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"should handle null), or provide a fallback like","depth":17,"bounds":{"left":0.19166666,"top":0.97888887,"width":0.5173611,"height":0.02111113},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon::createFromTimestamp(0)","depth":18,"bounds":{"left":0.36423612,"top":1.0,"width":0.14930555,"height":-0.004999995},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.51666665,"top":1.0,"width":0.0027777778,"height":-0.0022221804},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Fix this →","depth":17,"bounds":{"left":0.19166666,"top":1.0,"width":0.043055557,"height":-0.043333292},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"bounds":{"left":0.19166666,"top":1.0,"width":0.043055557,"height":-0.043333292},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Minor Issues","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Minor Issues","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. Double call to","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix()","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"prefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() ?","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() .","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"''","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"getLogPrefix()","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is called twice. Trivial but unnecessary — capture the result in a variable first.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"command doesn't guard HubSpot when","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{team}","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"argument is given —","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/SyncObjects.php:40-41","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teams","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[] = Team::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"idOrUuId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// no provider check","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Running","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-objects <hubspot-team-id>","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dispatches a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsJob","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Design Notes","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Design Notes","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Scheduler placement","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is placed inside","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"scheduleEveryFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"but uses a manual cron","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"instead of","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->everyFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SQS visibility timeout","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncHubspotObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"uses 1 hour (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"60 * 60","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") vs","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s 6 hours (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"60 * 60 * 6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). Appropriate given the more frequent, presumably lighter runs.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Stagger delay","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"yalokin-jiminny","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"force-pushed","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"force-pushed","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"the","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20553-debug-crm-sync-delays","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"branch from","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"f6c0ec3","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"f6c0ec3","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"5928f60","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
1934927101933781132
|
8380481863211482445
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (
60 * 60 * 6
). Appropriate given the more frequent, presumably lighter runs.
Stagger delay
: HubSpot uses 1s between teams vs 2s for general sync. Fine for 5-min runs, though worth monitoring if the number of HubSpot teams grows large enough that the last job barely fits within the 5-minute window.
Add or remove reactions
@yalokin-jiminny
yalokin-jiminny
yalokin-jiminny
force-pushed
force-pushed
the
JY-20553-debug-crm-sync-delays
branch from
f6c0ec3
f6c0ec3
to
5928f60...
|
60913
|
|
60913
|
1312
|
27
|
2026-04-21T06:25:05.642151+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-21/1776 /Users/lukas/.screenpipe/data/data/2026-04-21/1776752705642_m1.jpg...
|
Firefox
|
JY-20701 | Reschedule HubSpot Sync Objects by yalo JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11989
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6787] Issue with reconnecting Zoho - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny MCP Connector - Product - Confluence","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny MCP Connector - Product - Confluence","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny Mail","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny Mail","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20500] Batch initial sync for Salesforce - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Feed — jiminny — Sentry","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Feed — jiminny — Sentry","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"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,"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,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (31)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"31","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (21)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"21","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Review requested","depth":15,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Review requested","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requested your review on this pull request.","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add your review","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add your review","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JY-20701 | Reschedule HubSpot Sync Objects","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11989","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Awaiting approval","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Awaiting approval","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 22 commits into","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701-reschedule-HubSpot-processing","depth":16,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701-reschedule-HubSpot-processing","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 949 additions & 97 deletions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (5)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (22)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (11)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Conversation","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago •","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"JIRA: JY-20701","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"JIRA:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20701","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20701","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Casus:","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Casus:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"We want more fluent curve of the jobs distribution (no spikes) in","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm-sync","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"queue","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Changes:","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changes:","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Remove HubSpot from SyncObjects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Create separate HubSpotSyncObjects that runs each 5 min instead of each 30","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Extract common code","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Improve processing Webhooks jobs distribution","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Optimize ImportOpportunityBatch:","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cache OpportunitySyncableFields, OwnerProfiles, RecordType","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"remove findContactByConfigurationAndId - Contact ID already exist","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"add timings log","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":16,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"@yalokin-jiminny","depth":13,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Author","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"yalokin-jiminny commented 4 days ago","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"yalokin-jiminny","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"yalokin-jiminny","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@claude","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add or remove reactions","depth":15,"help_text":"","role_description":"summary","subrole":"AXSummary","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"react with eyes","depth":14,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"👀","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@claude","depth":13,"bounds":{"left":0.14097223,"top":0.0,"width":0.027777778,"height":0.044444446},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":14,"bounds":{"left":0.7125,"top":0.0,"width":0.016666668,"height":0.04111111},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"claude bot commented 4 days ago •","depth":13,"bounds":{"left":0.19166666,"top":0.0,"width":0.50416666,"height":0.041666668},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"claude","depth":15,"bounds":{"left":0.19166666,"top":0.0,"width":0.03125,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"claude","depth":16,"bounds":{"left":0.19166666,"top":0.0,"width":0.03125,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":16,"bounds":{"left":0.23020834,"top":0.0,"width":0.013541667,"height":0.016666668},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":14,"bounds":{"left":0.25138888,"top":0.0,"width":0.053125,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"4 days ago","depth":14,"bounds":{"left":0.30729166,"top":0.0,"width":0.049305554,"height":0.023333333},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 days ago","depth":16,"bounds":{"left":0.30729166,"top":0.0,"width":0.049305554,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":16,"bounds":{"left":0.359375,"top":0.0,"width":0.004513889,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":16,"bounds":{"left":0.36666667,"top":0.0,"width":0.042013887,"height":0.023333333},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":18,"bounds":{"left":0.36666667,"top":0.0,"width":0.030902777,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Claude finished","depth":18,"bounds":{"left":0.19166666,"top":0.0,"width":0.07534722,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@yalokin-jiminny","depth":18,"bounds":{"left":0.26701388,"top":0.0,"width":0.08020833,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"@yalokin-jiminny","depth":19,"bounds":{"left":0.26701388,"top":0.0,"width":0.08020833,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s task in 1m 59s","depth":18,"bounds":{"left":0.3472222,"top":0.0,"width":0.07847222,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"——","depth":17,"bounds":{"left":0.42569444,"top":0.0,"width":0.022222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"View job","depth":17,"bounds":{"left":0.44791666,"top":0.0,"width":0.038194444,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"View job","depth":18,"bounds":{"left":0.44791666,"top":0.0,"width":0.038194444,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Code Review","depth":16,"bounds":{"left":0.19166666,"top":0.04388889,"width":0.5375,"height":0.02388889},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Code Review","depth":17,"bounds":{"left":0.19166666,"top":0.044444446,"width":0.07361111,"height":0.023333333},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall the approach is clean — extracting the shared logic into","depth":17,"bounds":{"left":0.19166666,"top":0.08777778,"width":0.28611112,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsCommandTrait","depth":18,"bounds":{"left":0.48125,"top":0.090555556,"width":0.11423611,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is a good call, and the scheduling design (offset cron at","depth":17,"bounds":{"left":0.19166666,"top":0.08777778,"width":0.5083333,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":01","depth":18,"bounds":{"left":0.3454861,"top":0.11388889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to avoid spikes) makes sense. I found two bugs and a few minor points.","depth":17,"bounds":{"left":0.3638889,"top":0.11111111,"width":0.32222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Bugs","depth":16,"bounds":{"left":0.19166666,"top":0.18944444,"width":0.5375,"height":0.024444444},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bugs","depth":17,"bounds":{"left":0.19166666,"top":0.19,"width":0.028819444,"height":0.023333333},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. Null dereference on","depth":18,"bounds":{"left":0.19166666,"top":0.2338889,"width":0.10451389,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig","depth":19,"bounds":{"left":0.29965279,"top":0.23666666,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"bounds":{"left":0.35277778,"top":0.2338889,"width":0.013541667,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:49-51","depth":19,"bounds":{"left":0.36979166,"top":0.23666666,"width":0.20416667,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.29222223,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.20763889,"top":0.29222223,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.25243056,"top":0.29222223,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.2673611,"top":0.29222223,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.27222222,"top":0.29222223,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.2923611,"top":0.29222223,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"bounds":{"left":0.30208334,"top":0.29222223,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.3170139,"top":0.29222223,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"bounds":{"left":0.20277777,"top":0.33055556,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.2125,"top":0.33055556,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.33055556,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.22743055,"top":0.33055556,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.27222222,"top":0.33055556,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"bounds":{"left":0.28229168,"top":0.33055556,"width":0.039930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"bounds":{"left":0.32222223,"top":0.33055556,"width":0.099305555,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"bounds":{"left":0.42152777,"top":0.33055556,"width":0.07986111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"bounds":{"left":0.5013889,"top":0.33055556,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.28166667,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"$team->crm","depth":18,"bounds":{"left":0.19479166,"top":0.3888889,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"can be null if the team has no CRM configuration record. Accessing","depth":17,"bounds":{"left":0.24791667,"top":0.3861111,"width":0.30729166,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->provider","depth":18,"bounds":{"left":0.55833334,"top":0.3888889,"width":0.05,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"on null will throw a fatal error. The existing","depth":17,"bounds":{"left":0.19166666,"top":0.3861111,"width":0.5277778,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":18,"bounds":{"left":0.27881944,"top":0.41222224,"width":0.05451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"job handles this by using","depth":17,"bounds":{"left":0.33680555,"top":0.40944445,"width":0.11666667,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"??","depth":18,"bounds":{"left":0.45694444,"top":0.41222224,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.47013888,"top":0.40944445,"width":0.00625,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$crmConfig->provider ?? 'unknown'","depth":18,"bounds":{"left":0.4798611,"top":0.41222224,"width":0.16423611,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"). The new job needs a null guard here:","depth":17,"bounds":{"left":0.19166666,"top":0.40944445,"width":0.51944447,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.49055555,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.20763889,"top":0.49055555,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.25243056,"top":0.49055555,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.2673611,"top":0.49055555,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.27222222,"top":0.49055555,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.2923611,"top":0.49055555,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm","depth":17,"bounds":{"left":0.30208334,"top":0.49055555,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.3170139,"top":0.49055555,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"bounds":{"left":0.20277777,"top":0.5288889,"width":0.009722223,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.2125,"top":0.5288889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.5288889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.22743055,"top":0.5288889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"===","depth":17,"bounds":{"left":0.27222222,"top":0.5288889,"width":0.025,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":17,"bounds":{"left":0.29722223,"top":0.5288889,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"||","depth":17,"bounds":{"left":0.3170139,"top":0.5288889,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.33715278,"top":0.5288889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.3420139,"top":0.5288889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.38680556,"top":0.5288889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider","depth":17,"bounds":{"left":0.396875,"top":0.5288889,"width":0.039583333,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"!== Configuration::","depth":17,"bounds":{"left":0.43645832,"top":0.5288889,"width":0.099652775,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PROVIDER_HUBSPOT","depth":17,"bounds":{"left":0.5361111,"top":0.5288889,"width":0.07951389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"bounds":{"left":0.20277777,"top":0.5288889,"width":0.42777777,"height":0.035555556},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.22256945,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"logger","depth":17,"bounds":{"left":0.22743055,"top":0.54833335,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.25729167,"top":0.54833335,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"warning","depth":17,"bounds":{"left":0.2673611,"top":0.54833335,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.30208334,"top":0.54833335,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.30729166,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[SyncHubspotObjects] Team has no HubSpot CRM config","depth":17,"bounds":{"left":0.31215277,"top":0.54833335,"width":0.25381944,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.5659722,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", [","depth":17,"bounds":{"left":0.5708333,"top":0.54833335,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.5857639,"top":0.54833335,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"team","depth":17,"bounds":{"left":0.59097224,"top":0.54833335,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"bounds":{"left":0.6107639,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=>","depth":17,"bounds":{"left":0.615625,"top":0.54833335,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.6357639,"top":0.54833335,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"bounds":{"left":0.640625,"top":0.54833335,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.66041666,"top":0.54833335,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"bounds":{"left":0.6704861,"top":0.54833335,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]);","depth":17,"bounds":{"left":0.20277777,"top":0.54833335,"width":0.5125,"height":0.035},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"return","depth":17,"bounds":{"left":0.22256945,"top":0.56722224,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";\n}","depth":17,"bounds":{"left":0.20277777,"top":0.56722224,"width":0.05451389,"height":0.035555556},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.48055556,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Fix this →","depth":17,"bounds":{"left":0.19166666,"top":0.6422222,"width":0.043055557,"height":0.018888889},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"bounds":{"left":0.19166666,"top":0.6422222,"width":0.043055557,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.","depth":18,"bounds":{"left":0.19166666,"top":0.7227778,"width":0.011458334,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$lastSyncedAt","depth":19,"bounds":{"left":0.20659722,"top":0.72555554,"width":0.06458333,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is nullable but","depth":18,"bounds":{"left":0.27430555,"top":0.7227778,"width":0.07083333,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":19,"bounds":{"left":0.34861112,"top":0.72555554,"width":0.10451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"requires","depth":18,"bounds":{"left":0.45625,"top":0.7227778,"width":0.04375,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":19,"bounds":{"left":0.5034722,"top":0.72555554,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"bounds":{"left":0.5364583,"top":0.7227778,"width":0.013888889,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Jobs/Crm/SyncHubspotObjects.php:85,88,116","depth":19,"bounds":{"left":0.19166666,"top":0.72555554,"width":0.42673612,"height":0.039444443},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.8038889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.20763889,"top":0.8038889,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"bounds":{"left":0.2673611,"top":0.8038889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.28229168,"top":0.8038889,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmConfig","depth":17,"bounds":{"left":0.28715277,"top":0.8038889,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.33194444,"top":0.8038889,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"last_synced_at","depth":17,"bounds":{"left":0.3420139,"top":0.8038889,"width":0.06979167,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"bounds":{"left":0.41180557,"top":0.8038889,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// can be null on first run","depth":17,"bounds":{"left":0.42673612,"top":0.8038889,"width":0.134375,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.20277777,"top":0.8233333,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.20763889,"top":0.8233333,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.2125,"top":0.8233333,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.20277777,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"bounds":{"left":0.20763889,"top":0.8422222,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"bounds":{"left":0.22743055,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects","depth":17,"bounds":{"left":0.2375,"top":0.8422222,"width":0.10451389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"bounds":{"left":0.3420139,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.346875,"top":0.8422222,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"bounds":{"left":0.35208333,"top":0.8422222,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"bounds":{"left":0.4017361,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.41180557,"top":0.8422222,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.41666666,"top":0.8422222,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"bounds":{"left":0.4763889,"top":0.8422222,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// ...","depth":17,"bounds":{"left":0.20277777,"top":0.88055557,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"private","depth":17,"bounds":{"left":0.20277777,"top":0.9,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"function","depth":17,"bounds":{"left":0.24236111,"top":0.9,"width":0.039930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncHubspotCrmObjects(","depth":17,"bounds":{"left":0.28229168,"top":0.9,"width":0.114583336,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"HubspotInterface","depth":17,"bounds":{"left":0.396875,"top":0.9,"width":0.07951389,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.48125,"top":0.9,"width":0.0052083335,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crmService","depth":17,"bounds":{"left":0.48645833,"top":0.9,"width":0.049652778,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":17,"bounds":{"left":0.5361111,"top":0.9,"width":0.010069445,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":17,"bounds":{"left":0.54618055,"top":0.9,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"bounds":{"left":0.58090276,"top":0.9,"width":0.0048611113,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"lastSyncedAt","depth":17,"bounds":{"left":0.5857639,"top":0.9,"width":0.059722222,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"):","depth":17,"bounds":{"left":0.6454861,"top":0.9,"width":0.014930556,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"void","depth":17,"bounds":{"left":0.66041666,"top":0.9,"width":0.02013889,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"bounds":{"left":0.7,"top":0.79388887,"width":0.023611112,"height":0.039444443},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"With","depth":17,"bounds":{"left":0.19166666,"top":0.95555556,"width":0.023263888,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"declare(strict_types=1)","depth":18,"bounds":{"left":0.21805556,"top":0.9583333,"width":0.114583336,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", passing","depth":17,"bounds":{"left":0.3361111,"top":0.95555556,"width":0.042708334,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":18,"bounds":{"left":0.38229167,"top":0.9583333,"width":0.019791666,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to a","depth":17,"bounds":{"left":0.40555555,"top":0.95555556,"width":0.022222223,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon","depth":18,"bounds":{"left":0.43125,"top":0.9583333,"width":0.029861111,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"-typed parameter throws a","depth":17,"bounds":{"left":0.4642361,"top":0.95555556,"width":0.12256944,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TypeError","depth":18,"bounds":{"left":0.5902778,"top":0.9583333,"width":0.044791665,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". A team that has never synced before will crash on the first run. The parameter should be","depth":17,"bounds":{"left":0.19166666,"top":0.95555556,"width":0.5229167,"height":0.04222222},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"?Carbon","depth":18,"bounds":{"left":0.52118057,"top":0.9816667,"width":0.034722224,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(and","depth":17,"bounds":{"left":0.559375,"top":0.97888887,"width":0.025694445,"height":0.018888889},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"syncOpportunities","depth":18,"bounds":{"left":0.5885417,"top":0.9816667,"width":0.084375,"height":0.016111111},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"should handle null), or provide a fallback like","depth":17,"bounds":{"left":0.19166666,"top":0.97888887,"width":0.5173611,"height":0.02111113},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Carbon::createFromTimestamp(0)","depth":18,"bounds":{"left":0.36423612,"top":1.0,"width":0.14930555,"height":-0.004999995},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"bounds":{"left":0.51666665,"top":1.0,"width":0.0027777778,"height":-0.0022221804},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Fix this →","depth":17,"bounds":{"left":0.19166666,"top":1.0,"width":0.043055557,"height":-0.043333292},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fix this →","depth":18,"bounds":{"left":0.19166666,"top":1.0,"width":0.043055557,"height":-0.043333292},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Minor Issues","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Minor Issues","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. Double call to","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix()","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"prefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() ?","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"this","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"getLogPrefix","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"() .","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"''","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":";","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"getLogPrefix()","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is called twice. Trivial but unnecessary — capture the result in a variable first.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"command doesn't guard HubSpot when","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{team}","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"argument is given —","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app/Console/Commands/Crm/SyncObjects.php:40-41","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") {","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teams","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[] = Team::","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"idOrUuId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"teamId","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":");","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"// no provider check","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Running","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-objects <hubspot-team-id>","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dispatches a","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjectsJob","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Design Notes","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Design Notes","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Scheduler placement","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"crm:sync-hubspot-objects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"is placed inside","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"scheduleEveryFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"but uses a manual cron","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"instead of","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"->everyFiveMinutes()","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SQS visibility timeout","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncHubspotObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"uses 1 hour (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"60 * 60","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") vs","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SyncObjects","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'s 6 hours (","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-2106354106279207560
|
8380481828855938377
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
[SRD-6787] Issue with reconnecting Zoho - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
Jiminny MCP Connector - Product - Confluence
Jiminny MCP Connector - Product - Confluence
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
[JY-20676] Notify the user if a Panorama prompts is deleted but is used in AJ Report - Jira
Jiminny Mail
Jiminny Mail
[JY-20500] Batch initial sync for Salesforce - Jira
[JY-20500] Batch initial sync for Salesforce - Jira
Feed — jiminny — Sentry
Feed — jiminny — Sentry
Jiminny
Jiminny
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
JY-20701 | Reschedule HubSpot Sync Objects by yalokin-jiminny · Pull Request #11989 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (31)
Pull requests
(
31
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (21)
Security and quality
(
21
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
Review requested
Review requested
yalokin-jiminny
yalokin-jiminny
requested your review on this pull request.
Add your review
Add your review
JY-20701 | Reschedule HubSpot Sync Objects #11989 Edit title
JY-20701 | Reschedule HubSpot Sync Objects
#
11989
Edit title
Awaiting approval
Awaiting approval
Code
Code
Open
yalokin-jiminny
yalokin-jiminny
wants to merge 22 commits into
master
master
from
JY-20701-reschedule-HubSpot-processing
JY-20701-reschedule-HubSpot-processing
Copy head branch name to clipboard
Lines changed: 949 additions & 97 deletions
Conversation (5)
Conversation
(
5
)
Commits (22)
Commits
(
22
)
Checks (3)
Checks
(
3
)
Files changed (11)
Files changed
(
11
)
Conversation
Conversation
@yalokin-jiminny
Show options
yalokin-jiminny commented 4 days ago •
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
•
edited
edited
JIRA: JY-20701
JIRA:
JY-20701
JY-20701
Casus:
Casus:
We want more fluent curve of the jobs distribution (no spikes) in
crm-sync
queue
The more frequent HubSpot processing will help to have less auto-scaling / down-scaling events, thus reducing the jobs wait times
Changes:
Changes:
Remove HubSpot from SyncObjects
Create separate HubSpotSyncObjects that runs each 5 min instead of each 30
Extract common code
Improve processing Webhooks jobs distribution
Optimize ImportOpportunityBatch:
cache OpportunitySyncableFields, OwnerProfiles, RecordType
remove findContactByConfigurationAndId - Contact ID already exist
add timings log
Add or remove reactions
@yalokin-jiminny
Show options
Author
yalokin-jiminny commented 4 days ago
yalokin-jiminny
yalokin-jiminny
commented
4 days ago
4 days ago
@claude
@claude
Add or remove reactions
react with eyes
👀
1
@claude
Show options
claude bot commented 4 days ago •
claude
claude
bot
commented
4 days ago
4 days ago
•
edited
edited
Claude finished
@yalokin-jiminny
@yalokin-jiminny
's task in 1m 59s
——
View job
View job
Code Review
Code Review
Overall the approach is clean — extracting the shared logic into
SyncObjectsCommandTrait
is a good call, and the scheduling design (offset cron at
:01
to avoid spikes) makes sense. I found two bugs and a few minor points.
Bugs
Bugs
1. Null dereference on
$crmConfig
—
app/Jobs/Crm/SyncHubspotObjects.php:49-51
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
Copy
$team->crm
can be null if the team has no CRM configuration record. Accessing
->provider
on null will throw a fatal error. The existing
SyncObjects
job handles this by using
??
(
$crmConfig->provider ?? 'unknown'
). The new job needs a null guard here:
$
crmConfig
=
$
team
->
crm
;
if
(
$
crmConfig
===
null
||
$
crmConfig
->
provider
!== Configuration::
PROVIDER_HUBSPOT
) {
$
logger
->
warning
(
'
[SyncHubspotObjects] Team has no HubSpot CRM config
'
, [
'
team
'
=>
$
this
->
teamId
]);
return
;
}
Copy
Fix this →
Fix this →
2.
$lastSyncedAt
is nullable but
syncHubspotCrmObjects
requires
Carbon
—
app/Jobs/Crm/SyncHubspotObjects.php:85,88,116
$
lastSyncedAt
=
$
crmConfig
->
last_synced_at
;
// can be null on first run
.
.
.
$
this
->
syncHubspotCrmObjects
(
$
crmService
,
$
lastSyncedAt
);
// ...
private
function
syncHubspotCrmObjects(
HubspotInterface
$
crmService
,
Carbon
$
lastSyncedAt
):
void
Copy
With
declare(strict_types=1)
, passing
null
to a
Carbon
-typed parameter throws a
TypeError
. A team that has never synced before will crash on the first run. The parameter should be
?Carbon
(and
syncOpportunities
should handle null), or provide a fallback like
Carbon::createFromTimestamp(0)
.
Fix this →
Fix this →
Minor Issues
Minor Issues
3. Double call to
getLogPrefix()
—
app/Console/Commands/Crm/Traits/SyncObjectsCommandTrait.php:52
$
prefix
=
$
this
->
getLogPrefix
() ?
$
this
->
getLogPrefix
() .
'
'
:
''
;
Copy
getLogPrefix()
is called twice. Trivial but unnecessary — capture the result in a variable first.
4.
SyncObjects
command doesn't guard HubSpot when
{team}
argument is given —
app/Console/Commands/Crm/SyncObjects.php:40-41
if
(
$
teamId
) {
$
teams
[] = Team::
idOrUuId
(
$
teamId
);
// no provider check
Copy
Running
crm:sync-objects <hubspot-team-id>
dispatches a
SyncObjectsJob
for a HubSpot team, which now silently warns and returns. From an operator perspective this is confusing — the command appears to accept the team but does nothing. Worth adding a provider check and a console error message pointing to
crm:sync-hubspot-objects
.
Design Notes
Design Notes
Scheduler placement
:
crm:sync-hubspot-objects
is placed inside
scheduleEveryFiveMinutes()
but uses a manual cron
'1,6,11,16,21,26,31,36,41,46,51,56 * * * *'
instead of
->everyFiveMinutes()
. The 1-minute offset is intentional (good thinking to stagger from the other jobs), but it's slightly inconsistent with the method's name. A comment explaining the offset would help future readers.
SQS visibility timeout
:
SyncHubspotObjects
uses 1 hour (
60 * 60
) vs
SyncObjects
's 6 hours (...
|
NULL
|
|
52590
|
1139
|
1
|
2026-04-20T07:24:08.110071+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-20/1776 /Users/lukas/.screenpipe/data/data/2026-04-20/1776669848110_m2.jpg...
|
Firefox
|
fix(security): composer dependency updates – 2026- fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11970
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Checks pending
Checks pending
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0018284575,"top":0.0518755,"width":0.07596409,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"bounds":{"left":0.0,"top":0.09497207,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.10614525,"width":0.09524601,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.12769353,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.13886672,"width":0.19963431,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.16041501,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.17158818,"width":0.15525267,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":4,"bounds":{"left":0.0,"top":0.19313647,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.20430966,"width":0.06981383,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":4,"bounds":{"left":0.0,"top":0.22585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.23703113,"width":0.10688165,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.2585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.2697526,"width":0.12915559,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.29130086,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.30247405,"width":0.014960106,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Product Growth Platform | Userpilot","depth":4,"bounds":{"left":0.0,"top":0.32402235,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Product Growth Platform | Userpilot","depth":5,"bounds":{"left":0.013297873,"top":0.33519554,"width":0.06200133,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Userpilot | Events","depth":4,"bounds":{"left":0.0,"top":0.3567438,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Userpilot | Events","depth":5,"bounds":{"left":0.013297873,"top":0.367917,"width":0.030418882,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.38946527,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.40063846,"width":0.2052859,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.39664805,"width":0.007978723,"height":0.01915403},"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.4237829,"width":0.07413564,"height":0.025538707},"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},"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},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Close bookmarks (⌘B)","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Bookmarks","depth":5,"bounds":{"left":0.083277926,"top":0.06943336,"width":0.026761968,"height":0.014764565},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bookmarks","depth":6,"bounds":{"left":0.083277926,"top":0.06943336,"width":0.026761968,"height":0.014764565},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close sidebar","depth":6,"bounds":{"left":0.1783577,"top":0.06424581,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXTextField","text":"Search bookmarks","depth":7,"bounds":{"left":0.082446806,"top":0.09976058,"width":0.107546546,"height":0.025538707},"help_text":"","role_description":"search text field","subrole":"AXSearchField","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (32)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"32","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (28)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"28","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 Edit title","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Checks pending","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks pending","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":16,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 23 additions & 23 deletions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (2)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (6)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"6","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (1)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":14,"bounds":{"left":0.40641624,"top":0.0726257,"width":0.011968086,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard","depth":14,"bounds":{"left":0.42503324,"top":0.058260176,"width":0.20428856,"height":0.042298485},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"fix(security): composer dependency updates – 2026-04-15","depth":16,"bounds":{"left":0.42503324,"top":0.05865922,"width":0.13397606,"height":0.01915403},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":17,"bounds":{"left":0.42503324,"top":0.06304868,"width":0.13397606,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":16,"bounds":{"left":0.5616689,"top":0.06304868,"width":0.0028257978,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":16,"bounds":{"left":0.56449467,"top":0.06304868,"width":0.012466756,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":18,"bounds":{"left":0.42503324,"top":0.08339984,"width":0.03873005,"height":0.011971269},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":19,"bounds":{"left":0.42503324,"top":0.08339984,"width":0.03873005,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":18,"bounds":{"left":0.46509308,"top":0.08339984,"width":0.057845745,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":18,"bounds":{"left":0.5242686,"top":0.08180367,"width":0.018450798,"height":0.015163607},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":19,"bounds":{"left":0.5262633,"top":0.083798885,"width":0.014461436,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":19,"bounds":{"left":0.5440492,"top":0.08339984,"width":0.00880984,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":19,"bounds":{"left":0.55418885,"top":0.08180367,"width":0.061502658,"height":0.015163607},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":20,"bounds":{"left":0.5561835,"top":0.083798885,"width":0.057513297,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":19,"bounds":{"left":0.61702126,"top":0.07821229,"width":0.00930851,"height":0.022346368},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Conversation","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@github-actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"github-actions bot commented 5 days ago •","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"5 days ago","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5 days ago","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Security dependency updates — composer — 2026-04-15","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Security dependency updates — composer — 2026-04-15","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"below.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CI run logs →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CI run logs →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Upgrade safety (changelog review)","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Upgrade safety (changelog review)","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall verdict:","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mixed","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(phpunit/phpunit) is listed under","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This does not replace CI, tests, or manual smoke checks before merge.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Fixed alerts","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-6422696510024420125
|
8380202207970994610
|
click
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Checks pending
Checks pending
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347...
|
NULL
|
|
52954
|
1149
|
16
|
2026-04-20T07:50:21.172588+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-20/1776 /Users/lukas/.screenpipe/data/data/2026-04-20/1776671421172_m2.jpg...
|
Firefox
|
fix(security): composer dependency updates – 2026- fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11970
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
Pipelines - jiminny/app
Pipelines - jiminny/app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Ready to merge
Ready to merge
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (3)
Checks
(
3
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454)....
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0018284575,"top":0.0518755,"width":0.07596409,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"bounds":{"left":0.0,"top":0.09497207,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.10614525,"width":0.09524601,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.12769353,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.13886672,"width":0.19963431,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.16041501,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.17158818,"width":0.15525267,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":4,"bounds":{"left":0.0,"top":0.19313647,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.20430966,"width":0.06981383,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":4,"bounds":{"left":0.0,"top":0.22585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.23703113,"width":0.10688165,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.2585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.2697526,"width":0.12915559,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.29130086,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.30247405,"width":0.014960106,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Product Growth Platform | Userpilot","depth":4,"bounds":{"left":0.0,"top":0.32402235,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Product Growth Platform | Userpilot","depth":5,"bounds":{"left":0.013297873,"top":0.33519554,"width":0.06200133,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Userpilot | Events","depth":4,"bounds":{"left":0.0,"top":0.3567438,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Userpilot | Events","depth":5,"bounds":{"left":0.013297873,"top":0.367917,"width":0.030418882,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.38946527,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.40063846,"width":0.2052859,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.39664805,"width":0.007978723,"height":0.01915403},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.42218676,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.43335995,"width":0.039228722,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.45650437,"width":0.07413564,"height":0.025538707},"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},"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},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Close bookmarks (⌘B)","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Bookmarks","depth":5,"bounds":{"left":0.083277926,"top":0.06943336,"width":0.026761968,"height":0.014764565},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bookmarks","depth":6,"bounds":{"left":0.083277926,"top":0.06943336,"width":0.026761968,"height":0.014764565},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close sidebar","depth":6,"bounds":{"left":0.1783577,"top":0.06424581,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXTextField","text":"Search bookmarks","depth":7,"bounds":{"left":0.082446806,"top":0.09976058,"width":0.107546546,"height":0.025538707},"help_text":"","role_description":"search text field","subrole":"AXSearchField","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (32)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"32","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (28)","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"28","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 Edit title","depth":13,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Ready to merge","depth":13,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ready to merge","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":16,"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 23 additions & 23 deletions","depth":14,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (2)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (3)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (1)","depth":16,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":14,"bounds":{"left":0.40641624,"top":0.0726257,"width":0.011968086,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard","depth":14,"bounds":{"left":0.42503324,"top":0.058260176,"width":0.20428856,"height":0.042298485},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"fix(security): composer dependency updates – 2026-04-15","depth":16,"bounds":{"left":0.42503324,"top":0.05865922,"width":0.13397606,"height":0.01915403},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":17,"bounds":{"left":0.42503324,"top":0.06304868,"width":0.13397606,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":16,"bounds":{"left":0.5616689,"top":0.06304868,"width":0.0028257978,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":16,"bounds":{"left":0.56449467,"top":0.06304868,"width":0.012466756,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":18,"bounds":{"left":0.42503324,"top":0.08339984,"width":0.03873005,"height":0.011971269},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":19,"bounds":{"left":0.42503324,"top":0.08339984,"width":0.03873005,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 2 commits into","depth":18,"bounds":{"left":0.46509308,"top":0.08339984,"width":0.057845745,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":18,"bounds":{"left":0.5242686,"top":0.08180367,"width":0.018450798,"height":0.015163607},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":19,"bounds":{"left":0.5262633,"top":0.083798885,"width":0.014461436,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":19,"bounds":{"left":0.5440492,"top":0.08339984,"width":0.00880984,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":19,"bounds":{"left":0.55418885,"top":0.08180367,"width":0.061502658,"height":0.015163607},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":20,"bounds":{"left":0.5561835,"top":0.083798885,"width":0.057513297,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":19,"bounds":{"left":0.61702126,"top":0.07821229,"width":0.00930851,"height":0.022346368},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Conversation","depth":12,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@github-actions","depth":12,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"github-actions bot commented 5 days ago •","depth":14,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions","depth":16,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"5 days ago","depth":15,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5 days ago","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Security dependency updates — composer — 2026-04-15","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Security dependency updates — composer — 2026-04-15","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"below.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CI run logs →","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CI run logs →","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Upgrade safety (changelog review)","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Upgrade safety (changelog review)","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall verdict:","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mixed","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":17,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(phpunit/phpunit) is listed under","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This does not replace CI, tests, or manual smoke checks before merge.","depth":18,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Fixed alerts","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Covered by bump to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#425).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-5132889851774493403
|
8380091226284502192
|
visual_change
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
Pipelines - jiminny/app
Pipelines - jiminny/app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Ready to merge
Ready to merge
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (2)
Commits
(
2
)
Checks (3)
Checks
(
3
)
Files changed (1)
Files changed
(
1
)
Open
fix(security): composer dependency updates – 2026-04-15 #11970 github-actions[bot] wants to merge 2 commits into master from secfix/composer-20260415 Copy head branch name to clipboard
fix(security): composer dependency updates – 2026-04-15
fix(security): composer dependency updates – 2026-04-15
#
11970
github-actions[bot]
github-actions[bot]
wants to merge 2 commits into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454)....
|
NULL
|
|
52577
|
1137
|
47
|
2026-04-20T07:23:14.000222+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-20/1776 /Users/lukas/.screenpipe/data/data/2026-04-20/1776669794000_m2.jpg...
|
Firefox
|
fix(security): composer dependency updates – 2026- fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app — Work...
|
1
|
github.com/jiminny/app/pull/11970
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Ready to merge
Ready to merge
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 1 commit into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (1)
Commits
(
1
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Skipped alerts
Skipped alerts
Alert
Package
Severity
CVE
Patched version...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0018284575,"top":0.0518755,"width":0.07596409,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"bounds":{"left":0.0,"top":0.09497207,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.10614525,"width":0.09524601,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.12769353,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.13886672,"width":0.19963431,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.16041501,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.17158818,"width":0.15525267,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":4,"bounds":{"left":0.0,"top":0.19313647,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20543] AJ Reports > Tracking - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.20430966,"width":0.06981383,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":4,"bounds":{"left":0.0,"top":0.22585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.23703113,"width":0.10688165,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.2585794,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.2697526,"width":0.12915559,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.29130086,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.30247405,"width":0.014960106,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Product Growth Platform | Userpilot","depth":4,"bounds":{"left":0.0,"top":0.32402235,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Product Growth Platform | Userpilot","depth":5,"bounds":{"left":0.013297873,"top":0.33519554,"width":0.06200133,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Userpilot | Events","depth":4,"bounds":{"left":0.0,"top":0.3567438,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Userpilot | Events","depth":5,"bounds":{"left":0.013297873,"top":0.367917,"width":0.030418882,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.38946527,"width":0.07962101,"height":0.032721467},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.40063846,"width":0.2052859,"height":0.010774142},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.39664805,"width":0.007978723,"height":0.01915403},"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.4237829,"width":0.07413564,"height":0.025538707},"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},"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},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"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.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Close bookmarks (⌘B)","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Bookmarks","depth":5,"bounds":{"left":0.083277926,"top":0.06943336,"width":0.026761968,"height":0.014764565},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Bookmarks","depth":6,"bounds":{"left":0.083277926,"top":0.06943336,"width":0.026761968,"height":0.014764565},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close sidebar","depth":6,"bounds":{"left":0.1783577,"top":0.06424581,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXTextField","text":"Search bookmarks","depth":7,"bounds":{"left":0.082446806,"top":0.09976058,"width":0.107546546,"height":0.025538707},"help_text":"","role_description":"search text field","subrole":"AXSearchField","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Skip to content","depth":6,"bounds":{"left":0.19547872,"top":0.0518755,"width":0.0003324468,"height":0.0007980846},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to content","depth":7,"bounds":{"left":0.19547872,"top":0.05347167,"width":0.0029920214,"height":0.21468475},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Open menu","depth":10,"bounds":{"left":0.20079787,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Homepage (g then d)","depth":9,"bounds":{"left":0.21542554,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"jiminny","depth":12,"bounds":{"left":0.2287234,"top":0.06464485,"width":0.018949468,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"jiminny","depth":14,"bounds":{"left":0.23071809,"top":0.07063048,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"app","depth":12,"bounds":{"left":0.2526596,"top":0.06464485,"width":0.017785905,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"app","depth":14,"bounds":{"left":0.25465426,"top":0.07063048,"width":0.008477394,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Search or jump to…","depth":9,"bounds":{"left":0.8171542,"top":0.06464485,"width":0.06565824,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Type","depth":12,"bounds":{"left":0.8294548,"top":0.07063048,"width":0.011801862,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":12,"bounds":{"left":0.84258646,"top":0.07222666,"width":0.002493351,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to search","depth":12,"bounds":{"left":0.8465758,"top":0.07063048,"width":0.021276595,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Chat with Copilot","depth":10,"bounds":{"left":0.88480717,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Open Copilot…","depth":9,"bounds":{"left":0.89511305,"top":0.06464485,"width":0.008643617,"height":0.025538707},"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Create new...","depth":9,"bounds":{"left":0.91173536,"top":0.06464485,"width":0.01662234,"height":0.025538707},"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Issues(g then i)","depth":9,"bounds":{"left":0.9310173,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Pull requests","depth":9,"bounds":{"left":0.94431514,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Repositories","depth":9,"bounds":{"left":0.95761305,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"You have unread notifications(g then n)","depth":9,"bounds":{"left":0.9709109,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open user navigation menu","depth":9,"bounds":{"left":0.98420876,"top":0.06464485,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Repository navigation","depth":9,"bounds":{"left":0.19514628,"top":0.051077414,"width":0.0003324468,"height":0.0007980846},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Repository navigation","depth":10,"bounds":{"left":0.19514628,"top":0.05387071,"width":0.0787899,"height":0.023144454},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":12,"bounds":{"left":0.20079787,"top":0.09936153,"width":0.025099734,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":14,"bounds":{"left":0.21160239,"top":0.10574621,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull requests (32)","depth":12,"bounds":{"left":0.22855718,"top":0.09936153,"width":0.05501995,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests","depth":14,"bounds":{"left":0.23919548,"top":0.10574621,"width":0.02925532,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"bounds":{"left":0.2711104,"top":0.113727055,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"32","depth":14,"bounds":{"left":0.2741024,"top":0.113727055,"width":0.0056515955,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"bounds":{"left":0.27975398,"top":0.113727055,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Agents","depth":12,"bounds":{"left":0.2862367,"top":0.09936153,"width":0.029089095,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Agents","depth":14,"bounds":{"left":0.29720744,"top":0.10574621,"width":0.01512633,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":12,"bounds":{"left":0.3179854,"top":0.09936153,"width":0.03025266,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":14,"bounds":{"left":0.32912233,"top":0.10574621,"width":0.015957447,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":12,"bounds":{"left":0.3508976,"top":0.09936153,"width":0.022938829,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":14,"bounds":{"left":0.36186835,"top":0.10574621,"width":0.009142287,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Security and quality (28)","depth":12,"bounds":{"left":0.37649602,"top":0.09936153,"width":0.070644945,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Security and quality","depth":14,"bounds":{"left":0.3882979,"top":0.10574621,"width":0.04255319,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":14,"bounds":{"left":0.4346742,"top":0.113727055,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"28","depth":14,"bounds":{"left":0.43766624,"top":0.113727055,"width":0.0056515955,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":14,"bounds":{"left":0.44331783,"top":0.113727055,"width":0.0018284575,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Insights","depth":12,"bounds":{"left":0.44980052,"top":0.09936153,"width":0.03125,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Insights","depth":14,"bounds":{"left":0.46110374,"top":0.10574621,"width":0.016788565,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":12,"bounds":{"left":0.4837101,"top":0.09936153,"width":0.032081116,"height":0.026336791},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":14,"bounds":{"left":0.4948471,"top":0.10574621,"width":0.017785905,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Important update","depth":10,"bounds":{"left":0.20910904,"top":0.14365523,"width":0.0003324468,"height":0.016759777},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Important update","depth":11,"bounds":{"left":0.20910904,"top":0.1452514,"width":0.039228722,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.","depth":10,"bounds":{"left":0.20910904,"top":0.1452514,"width":0.2159242,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Review this update","depth":10,"bounds":{"left":0.42503324,"top":0.1452514,"width":0.04055851,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Review this update","depth":11,"bounds":{"left":0.42503324,"top":0.1452514,"width":0.04055851,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and manage your preferences in your","depth":10,"bounds":{"left":0.46559176,"top":0.1452514,"width":0.08261303,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"GitHub account settings","depth":10,"bounds":{"left":0.5482048,"top":0.1452514,"width":0.05219415,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"GitHub account settings","depth":11,"bounds":{"left":0.5482048,"top":0.1452514,"width":0.05219415,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"bounds":{"left":0.60039896,"top":0.1452514,"width":0.0013297872,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dismiss banner","depth":9,"bounds":{"left":0.9865359,"top":0.13886672,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"fix(security): composer dependency updates – 2026-04-15 #11970 Edit title","depth":13,"bounds":{"left":0.3957779,"top":0.1915403,"width":0.32081118,"height":0.06384677},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fix(security): composer dependency updates – 2026-04-15","depth":14,"bounds":{"left":0.3957779,"top":0.19233839,"width":0.27194148,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#","depth":15,"bounds":{"left":0.3984375,"top":0.22426178,"width":0.006482713,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"11970","depth":15,"bounds":{"left":0.40492022,"top":0.22426178,"width":0.027759308,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit title","depth":14,"bounds":{"left":0.4340093,"top":0.22665602,"width":0.010638298,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Ready to merge","depth":13,"bounds":{"left":0.71924865,"top":0.19832402,"width":0.05119681,"height":0.025538707},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ready to merge","depth":15,"bounds":{"left":0.7315492,"top":0.20430966,"width":0.034574468,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Code","depth":13,"bounds":{"left":0.77177525,"top":0.19832402,"width":0.02825798,"height":0.025538707},"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":15,"bounds":{"left":0.77609706,"top":0.20430966,"width":0.011635638,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Open","depth":13,"bounds":{"left":0.40641624,"top":0.2677574,"width":0.011968086,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions[bot]","depth":15,"bounds":{"left":0.42503324,"top":0.26456505,"width":0.044215426,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions[bot]","depth":16,"bounds":{"left":0.42503324,"top":0.2661612,"width":0.044215426,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"wants to merge 1 commit into","depth":15,"bounds":{"left":0.47057846,"top":0.2661612,"width":0.06333112,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"master","depth":15,"bounds":{"left":0.53523934,"top":0.264166,"width":0.018450798,"height":0.017557861},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"master","depth":16,"bounds":{"left":0.53723407,"top":0.26735833,"width":0.014461436,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"from","depth":16,"bounds":{"left":0.55502,"top":0.2661612,"width":0.009973404,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"secfix/composer-20260415","depth":16,"bounds":{"left":0.56632316,"top":0.264166,"width":0.061668884,"height":0.017557861},"role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secfix/composer-20260415","depth":17,"bounds":{"left":0.56831783,"top":0.26735833,"width":0.057679523,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy head branch name to clipboard","depth":16,"bounds":{"left":0.6293218,"top":0.26177174,"width":0.00930851,"height":0.022346368},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Lines changed: 23 additions & 23 deletions","depth":14,"bounds":{"left":0.76761967,"top":0.3180367,"width":0.019946808,"height":0.11412609},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Conversation (3)","depth":16,"bounds":{"left":0.3957779,"top":0.30007982,"width":0.057347074,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Conversation","depth":17,"bounds":{"left":0.40940824,"top":0.30965683,"width":0.028091755,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.4474734,"top":0.30965683,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3","depth":18,"bounds":{"left":0.4504654,"top":0.30965683,"width":0.0028257978,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.45329124,"top":0.30965683,"width":0.0018284575,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Commits (1)","depth":16,"bounds":{"left":0.453125,"top":0.30007982,"width":0.047706116,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Commits","depth":17,"bounds":{"left":0.46675533,"top":0.30965683,"width":0.019115692,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.49517953,"top":0.30965683,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":18,"bounds":{"left":0.49817154,"top":0.30965683,"width":0.0019946808,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.50016624,"top":0.30965683,"width":0.0018284575,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Checks (6)","depth":16,"bounds":{"left":0.5008311,"top":0.30007982,"width":0.04504654,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Checks","depth":17,"bounds":{"left":0.51446146,"top":0.30965683,"width":0.015791224,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.54022604,"top":0.30965683,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"6","depth":18,"bounds":{"left":0.5432181,"top":0.30965683,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.5462101,"top":0.30965683,"width":0.0016622341,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Files changed (1)","depth":16,"bounds":{"left":0.54587764,"top":0.30007982,"width":0.058344416,"height":0.031923383},"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Files changed","depth":17,"bounds":{"left":0.55950797,"top":0.30965683,"width":0.029920213,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(","depth":18,"bounds":{"left":0.59857047,"top":0.30965683,"width":0.0029920214,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":18,"bounds":{"left":0.6015625,"top":0.30965683,"width":0.0021609042,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":18,"bounds":{"left":0.6037234,"top":0.30965683,"width":0.0018284575,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Conversation","depth":12,"bounds":{"left":0.3957779,"top":0.34557062,"width":0.0003324468,"height":0.0007980846},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation","depth":13,"bounds":{"left":0.3957779,"top":0.34836394,"width":0.048204787,"height":0.023144454},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"@github-actions","depth":12,"bounds":{"left":0.3957779,"top":0.34557062,"width":0.013297873,"height":0.031923383},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show options","depth":15,"bounds":{"left":0.6693817,"top":0.3463687,"width":0.007978723,"height":0.02952913},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"github-actions bot commented 5 days ago •","depth":14,"bounds":{"left":0.42004654,"top":0.3463687,"width":0.24135639,"height":0.029928172},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"github-actions","depth":16,"bounds":{"left":0.42004654,"top":0.35434955,"width":0.03307846,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"github-actions","depth":17,"bounds":{"left":0.42004654,"top":0.35434955,"width":0.03307846,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"bot","depth":17,"bounds":{"left":0.4566157,"top":0.35594574,"width":0.006482713,"height":0.011971269},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"commented","depth":15,"bounds":{"left":0.46675533,"top":0.35434955,"width":0.02543218,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"5 days ago","depth":15,"bounds":{"left":0.49351728,"top":0.3527534,"width":0.023603724,"height":0.016759777},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"5 days ago","depth":17,"bounds":{"left":0.49351728,"top":0.35434955,"width":0.023603724,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"•","depth":17,"bounds":{"left":0.5184508,"top":0.35434955,"width":0.0019946808,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"edited","depth":17,"bounds":{"left":0.52177525,"top":0.3527534,"width":0.020279255,"height":0.016759777},"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"edited","depth":19,"bounds":{"left":0.52177525,"top":0.35434955,"width":0.014960106,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Security dependency updates — composer — 2026-04-15","depth":16,"bounds":{"left":0.42004654,"top":0.38986433,"width":0.25731382,"height":0.026735835},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Security dependency updates — composer — 2026-04-15","depth":17,"bounds":{"left":0.42004654,"top":0.39026338,"width":0.18633644,"height":0.019952115},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see","depth":17,"bounds":{"left":0.42004654,"top":0.4309657,"width":0.23869681,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":18,"bounds":{"left":0.47822472,"top":0.44772545,"width":0.026097074,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"below.","depth":17,"bounds":{"left":0.5043218,"top":0.44772545,"width":0.016289894,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CI run logs →","depth":17,"bounds":{"left":0.5206117,"top":0.44772545,"width":0.028590426,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CI run logs →","depth":18,"bounds":{"left":0.5206117,"top":0.44772545,"width":0.028590426,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Upgrade safety (changelog review)","depth":16,"bounds":{"left":0.42004654,"top":0.4820431,"width":0.25731382,"height":0.017557861},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Upgrade safety (changelog review)","depth":17,"bounds":{"left":0.42004654,"top":0.48244214,"width":0.09591091,"height":0.016759777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overall verdict:","depth":18,"bounds":{"left":0.42004654,"top":0.5139665,"width":0.034075797,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Mixed","depth":18,"bounds":{"left":0.4554521,"top":0.5139665,"width":0.013464096,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert","depth":17,"bounds":{"left":0.46891624,"top":0.5139665,"width":0.16988032,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#463","depth":17,"bounds":{"left":0.63879657,"top":0.5139665,"width":0.011635638,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#463","depth":18,"bounds":{"left":0.63879657,"top":0.5139665,"width":0.011635638,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(phpunit/phpunit) is listed under","depth":17,"bounds":{"left":0.42004654,"top":0.5139665,"width":0.25116357,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":18,"bounds":{"left":0.4709109,"top":0.53072625,"width":0.032413565,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.","depth":17,"bounds":{"left":0.42004654,"top":0.53072625,"width":0.2435173,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This does not replace CI, tests, or manual smoke checks before merge.","depth":18,"bounds":{"left":0.42004654,"top":0.547486,"width":0.2534907,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Fixed alerts","depth":16,"bounds":{"left":0.42004654,"top":0.59856343,"width":0.25731382,"height":0.017557861},"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Fixed alerts","depth":17,"bounds":{"left":0.42004654,"top":0.5989625,"width":0.031416222,"height":0.016759777},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"bounds":{"left":0.42486703,"top":0.6444533,"width":0.010970744,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"bounds":{"left":0.4509641,"top":0.6444533,"width":0.018949468,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"bounds":{"left":0.48470744,"top":0.6444533,"width":0.018450798,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"bounds":{"left":0.5270944,"top":0.6444533,"width":0.009474734,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"bounds":{"left":0.5611702,"top":0.6360734,"width":0.018450798,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"bounds":{"left":0.5894282,"top":0.6444533,"width":0.024102394,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"bounds":{"left":0.6409575,"top":0.6444533,"width":0.013297873,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"bounds":{"left":0.4247008,"top":0.7134876,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"bounds":{"left":0.4247008,"top":0.7134876,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"bounds":{"left":0.44514626,"top":0.70510775,"width":0.018949468,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"bounds":{"left":0.48470744,"top":0.7134876,"width":0.00930851,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"bounds":{"left":0.5121343,"top":0.7134876,"width":0.03939495,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"bounds":{"left":0.5121343,"top":0.7134876,"width":0.03939495,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"bounds":{"left":0.56050533,"top":0.7134876,"width":0.01412899,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"bounds":{"left":0.5894282,"top":0.7134876,"width":0.017785905,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"bounds":{"left":0.5894282,"top":0.7134876,"width":0.017785905,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"bounds":{"left":0.6225067,"top":0.67996806,"width":0.049534574,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"bounds":{"left":0.6225067,"top":0.6967279,"width":0.048537236,"height":0.04708699},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"bounds":{"left":0.6241689,"top":0.7490024,"width":0.035738032,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"bounds":{"left":0.66140294,"top":0.7470072,"width":0.0013297872,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"bounds":{"left":0.4247008,"top":0.8160415,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"bounds":{"left":0.4247008,"top":0.8160415,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"bounds":{"left":0.44514626,"top":0.8076616,"width":0.01861702,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"bounds":{"left":0.48470744,"top":0.8160415,"width":0.00930851,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"bounds":{"left":0.5121343,"top":0.8160415,"width":0.036402926,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"bounds":{"left":0.5121343,"top":0.8160415,"width":0.036402926,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"bounds":{"left":0.56050533,"top":0.8160415,"width":0.01412899,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"bounds":{"left":0.5894282,"top":0.8160415,"width":0.017785905,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"bounds":{"left":0.5894282,"top":0.8160415,"width":0.017785905,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"bounds":{"left":0.6225067,"top":0.7741421,"width":0.049534574,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"bounds":{"left":0.6225067,"top":0.79090184,"width":0.048537236,"height":0.06384677},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"bounds":{"left":0.6241689,"top":0.8599362,"width":0.035738032,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"bounds":{"left":0.66140294,"top":0.8579409,"width":0.0013297872,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"bounds":{"left":0.4247008,"top":0.9185954,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"bounds":{"left":0.4247008,"top":0.9185954,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"bounds":{"left":0.44514626,"top":0.9102155,"width":0.022273935,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"bounds":{"left":0.48470744,"top":0.9185954,"width":0.00930851,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"bounds":{"left":0.5121343,"top":0.9185954,"width":0.03939495,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"bounds":{"left":0.5121343,"top":0.9185954,"width":0.03939495,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"bounds":{"left":0.56050533,"top":0.9185954,"width":0.01412899,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"bounds":{"left":0.5894282,"top":0.9185954,"width":0.017785905,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"bounds":{"left":0.5894282,"top":0.9185954,"width":0.017785905,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"bounds":{"left":0.6225067,"top":0.8850758,"width":0.049534574,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"bounds":{"left":0.6225067,"top":0.9018356,"width":0.048537236,"height":0.04708699},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"bounds":{"left":0.6225067,"top":0.95211494,"width":0.009973404,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"bounds":{"left":0.63248,"top":0.95211494,"width":0.015957447,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"bounds":{"left":0.4247008,"top":1.0,"width":0.011469414,"height":-0.029529095},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"bounds":{"left":0.4247008,"top":1.0,"width":0.011469414,"height":-0.029529095},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"bounds":{"left":0.44514626,"top":1.0,"width":0.02925532,"height":-0.021149278},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"bounds":{"left":0.48470744,"top":1.0,"width":0.017287234,"height":-0.029529095},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"bounds":{"left":0.5121343,"top":1.0,"width":0.03939495,"height":-0.029529095},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"bounds":{"left":0.5121343,"top":1.0,"width":0.03939495,"height":-0.029529095},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"bounds":{"left":0.56050533,"top":1.0,"width":0.011303191,"height":-0.029529095},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"bounds":{"left":0.5894282,"top":1.0,"width":0.017785905,"height":-0.029529095},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"bounds":{"left":0.5894282,"top":1.0,"width":0.017785905,"height":-0.029529095},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"bounds":{"left":0.6225067,"top":0.9792498,"width":0.049534574,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via","depth":20,"bounds":{"left":0.6225067,"top":0.9960096,"width":0.048537236,"height":0.0039904118},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"bounds":{"left":0.6241689,"top":1.0,"width":0.035738032,"height":-0.08180368},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"bounds":{"left":0.66140294,"top":1.0,"width":0.0013297872,"height":-0.07980847},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Covered by bump to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#425).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"bounds":{"left":0.42486703,"top":0.6444533,"width":0.010970744,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#457","depth":20,"bounds":{"left":0.4247008,"top":0.7134876,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#457","depth":21,"bounds":{"left":0.4247008,"top":0.7134876,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#434","depth":20,"bounds":{"left":0.4247008,"top":0.8160415,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#434","depth":21,"bounds":{"left":0.4247008,"top":0.8160415,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#425","depth":20,"bounds":{"left":0.4247008,"top":0.9185954,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#425","depth":21,"bounds":{"left":0.4247008,"top":0.9185954,"width":0.011469414,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#429","depth":20,"bounds":{"left":0.4247008,"top":1.0,"width":0.011469414,"height":-0.029529095},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#429","depth":21,"bounds":{"left":0.4247008,"top":1.0,"width":0.011469414,"height":-0.029529095},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"#454","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"#454","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"bounds":{"left":0.4509641,"top":0.6444533,"width":0.018949468,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"laravel/passport","depth":20,"bounds":{"left":0.44514626,"top":0.70510775,"width":0.018949468,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"google/protobuf","depth":20,"bounds":{"left":0.44514626,"top":0.8076616,"width":0.01861702,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"bounds":{"left":0.44514626,"top":0.9102155,"width":0.022273935,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"league/commonmark","depth":20,"bounds":{"left":0.44514626,"top":1.0,"width":0.02925532,"height":-0.021149278},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"phpseclib/phpseclib","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"bounds":{"left":0.48470744,"top":0.6444533,"width":0.018450798,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"bounds":{"left":0.48470744,"top":0.7134876,"width":0.00930851,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"bounds":{"left":0.48470744,"top":0.8160415,"width":0.00930851,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"high","depth":20,"bounds":{"left":0.48470744,"top":0.9185954,"width":0.00930851,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"medium","depth":20,"bounds":{"left":0.48470744,"top":1.0,"width":0.017287234,"height":-0.029529095},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"low","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"bounds":{"left":0.5270944,"top":0.6444533,"width":0.009474734,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-39976","depth":20,"bounds":{"left":0.5121343,"top":0.7134876,"width":0.03939495,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-39976","depth":21,"bounds":{"left":0.5121343,"top":0.7134876,"width":0.03939495,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-6409","depth":20,"bounds":{"left":0.5121343,"top":0.8160415,"width":0.036402926,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-6409","depth":21,"bounds":{"left":0.5121343,"top":0.8160415,"width":0.036402926,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-32935","depth":20,"bounds":{"left":0.5121343,"top":0.9185954,"width":0.03939495,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-32935","depth":21,"bounds":{"left":0.5121343,"top":0.9185954,"width":0.03939495,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-33347","depth":20,"bounds":{"left":0.5121343,"top":1.0,"width":0.03939495,"height":-0.029529095},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-33347","depth":21,"bounds":{"left":0.5121343,"top":1.0,"width":0.03939495,"height":-0.029529095},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"CVE-2026-40194","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CVE-2026-40194","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"bounds":{"left":0.5611702,"top":0.6360734,"width":0.018450798,"height":0.030327214},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"13.7.1","depth":20,"bounds":{"left":0.56050533,"top":0.7134876,"width":0.01412899,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"4.33.6","depth":20,"bounds":{"left":0.56050533,"top":0.8160415,"width":0.01412899,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.50","depth":20,"bounds":{"left":0.56050533,"top":0.9185954,"width":0.01412899,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2.8.2","depth":20,"bounds":{"left":0.56050533,"top":1.0,"width":0.011303191,"height":-0.029529095},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3.0.51","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Changelog","depth":20,"bounds":{"left":0.5894282,"top":0.6444533,"width":0.024102394,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"bounds":{"left":0.5894282,"top":0.7134876,"width":0.017785905,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"bounds":{"left":0.5894282,"top":0.7134876,"width":0.017785905,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"bounds":{"left":0.5894282,"top":0.8160415,"width":0.017785905,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"bounds":{"left":0.5894282,"top":0.8160415,"width":0.017785905,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"bounds":{"left":0.5894282,"top":0.9185954,"width":0.017785905,"height":0.013567438},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"bounds":{"left":0.5894282,"top":0.9185954,"width":0.017785905,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"bounds":{"left":0.5894282,"top":1.0,"width":0.017785905,"height":-0.029529095},"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"bounds":{"left":0.5894282,"top":1.0,"width":0.017785905,"height":-0.029529095},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"releases","depth":20,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"releases","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notes","depth":20,"bounds":{"left":0.6409575,"top":0.6444533,"width":0.013297873,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"bounds":{"left":0.6225067,"top":0.67996806,"width":0.049534574,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via","depth":20,"bounds":{"left":0.6225067,"top":0.6967279,"width":0.048537236,"height":0.04708699},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"bounds":{"left":0.6241689,"top":0.7490024,"width":0.035738032,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"bounds":{"left":0.66140294,"top":0.7470072,"width":0.0013297872,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"bounds":{"left":0.6225067,"top":0.7741421,"width":0.049534574,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via","depth":20,"bounds":{"left":0.6225067,"top":0.79090184,"width":0.048537236,"height":0.06384677},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"bounds":{"left":0.6241689,"top":0.8599362,"width":0.035738032,"height":0.011572227},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"bounds":{"left":0.66140294,"top":0.8579409,"width":0.0013297872,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"bounds":{"left":0.6225067,"top":0.8850758,"width":0.049534574,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also","depth":20,"bounds":{"left":0.6225067,"top":0.9018356,"width":0.048537236,"height":0.04708699},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"bounds":{"left":0.6225067,"top":0.95211494,"width":0.009973404,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#454).","depth":20,"bounds":{"left":0.63248,"top":0.95211494,"width":0.015957447,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"bounds":{"left":0.6225067,"top":0.9792498,"width":0.049534574,"height":0.013567438},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via","depth":20,"bounds":{"left":0.6225067,"top":0.9960096,"width":0.048537236,"height":0.0039904118},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"composer update","depth":21,"bounds":{"left":0.6241689,"top":1.0,"width":0.035738032,"height":-0.08180368},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":20,"bounds":{"left":0.66140294,"top":1.0,"width":0.0013297872,"height":-0.07980847},"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Breaking-change risk:","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none observed (patch/minor). Covered by bump to 3.0.51 (also","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fixes","depth":21,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"#425).","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Skipped alerts","depth":16,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skipped alerts","depth":17,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Alert","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Package","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Severity","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CVE","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Patched version","depth":20,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
8249704731925293129
|
8380090676262087866
|
visual_change
|
accessibility
|
NULL
|
Platform Sprint 2 Q2 - Platform Team - Scrum Board Platform Sprint 2 Q2 - Platform Team - Scrum Board - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20698 handle failed field sync on playbook import activity types by LakyLak · Pull Request #11988 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
JY-20692 change confirmation parameter by LakyLak · Pull Request #11986 · jiminny/app
[JY-20543] AJ Reports > Tracking - Jira
[JY-20543] AJ Reports > Tracking - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
[JY-18909] [Part2] Automated reports with Ask Jiminny - Jira
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
Ask Jiminny Reports by nikolay-yankov · Pull Request #11894 · jiminny/app
New Tab
New Tab
Product Growth Platform | Userpilot
Product Growth Platform | Userpilot
Userpilot | Events
Userpilot | Events
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
fix(security): composer dependency updates – 2026-04-15 by github-actions[bot] · Pull Request #11970 · jiminny/app
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Close bookmarks (⌘B)
Bookmarks
Bookmarks
Close sidebar
Search bookmarks
Skip to content
Skip to content
Open menu
Homepage (g then d)
jiminny
jiminny
app
app
Search or jump to…
Type
/
to search
Chat with Copilot
Open Copilot…
Create new...
Issues(g then i)
Pull requests
Repositories
You have unread notifications(g then n)
Open user navigation menu
Repository navigation
Repository navigation
Code
Code
Pull requests (32)
Pull requests
(
32
)
Agents
Agents
Actions
Actions
Wiki
Wiki
Security and quality (28)
Security and quality
(
28
)
Insights
Insights
Settings
Settings
Important update
Important update
On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out.
Review this update
Review this update
and manage your preferences in your
GitHub account settings
GitHub account settings
.
Dismiss banner
fix(security): composer dependency updates – 2026-04-15 #11970 Edit title
fix(security): composer dependency updates – 2026-04-15
#
11970
Edit title
Ready to merge
Ready to merge
Code
Code
Open
github-actions[bot]
github-actions[bot]
wants to merge 1 commit into
master
master
from
secfix/composer-20260415
secfix/composer-20260415
Copy head branch name to clipboard
Lines changed: 23 additions & 23 deletions
Conversation (3)
Conversation
(
3
)
Commits (1)
Commits
(
1
)
Checks (6)
Checks
(
6
)
Files changed (1)
Files changed
(
1
)
Conversation
Conversation
@github-actions
Show options
github-actions bot commented 5 days ago •
github-actions
github-actions
bot
commented
5 days ago
5 days ago
•
edited
edited
Security dependency updates — composer — 2026-04-15
Security dependency updates — composer — 2026-04-15
This PR was opened automatically by the secfix bot. For this ecosystem, one commit carries every dependency upgrade from this run; see
Fixed alerts
below.
CI run logs →
CI run logs →
Upgrade safety (changelog review)
Upgrade safety (changelog review)
Overall verdict:
Mixed
— All previously-actionable alerts were fixed as safe patch/minor bumps. Alert
#463
#463
(phpunit/phpunit) is listed under
Skipped alerts
: the patched version (12.5.22) requires a major version jump from 11.x that includes documented breaking API removals and requires manual migration.
This does not replace CI, tests, or manual smoke checks before merge.
Fixed alerts
Fixed alerts
Alert
Package
Severity
CVE
Patched version
Changelog
Notes
#457
#457
laravel/passport
high
CVE-2026-39976
CVE-2026-39976
13.7.1
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
#434
#434
google/protobuf
high
CVE-2026-6409
CVE-2026-6409
4.33.6
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
#425
#425
phpseclib/phpseclib
high
CVE-2026-32935
CVE-2026-32935
3.0.50
releases
releases
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
#429
#429
league/commonmark
medium
CVE-2026-33347
CVE-2026-33347
2.8.2
releases
releases
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
#454
#454
phpseclib/phpseclib
low
CVE-2026-40194
CVE-2026-40194
3.0.51
releases
releases
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Alert
#457
#457
#434
#434
#425
#425
#429
#429
#454
#454
Package
laravel/passport
google/protobuf
phpseclib/phpseclib
league/commonmark
phpseclib/phpseclib
Severity
high
high
high
medium
low
CVE
CVE-2026-39976
CVE-2026-39976
CVE-2026-6409
CVE-2026-6409
CVE-2026-32935
CVE-2026-32935
CVE-2026-33347
CVE-2026-33347
CVE-2026-40194
CVE-2026-40194
Patched version
13.7.1
4.33.6
3.0.50
2.8.2
3.0.51
Changelog
releases
releases
releases
releases
releases
releases
releases
releases
releases
releases
Notes
Breaking-change risk:
none observed (patch/minor). Bumped from v13.6.0 to v13.7.4 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Transitive dep; bumped from v4.33.5 to v4.33.6 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Bumped from 3.0.49 to 3.0.51 (also
fixes
#454).
Breaking-change risk:
none observed (patch/minor). Transitive dep (via laravel/framework); bumped from 2.8.1 to 2.8.2 via
composer update
.
Breaking-change risk:
none observed (patch/minor). Covered by bump to 3.0.51 (also
fixes
#425).
Skipped alerts
Skipped alerts
Alert
Package
Severity
CVE
Patched version...
|
NULL
|