|
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
4
1
7
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Traits\RequiresUUID;
/**
* Jiminny\Models\AutomatedReport
*
* @property int $id
* @property string $uuid
* @property int $team_id
* @property string $type
* @property bool $status
* @property string $frequency
* @property Carbon|null $from
* @property Carbon|null $to
* @property int|null $deal_value_min
* @property int|null $deal_value_max
* @property array $call_types
* @property array $media_types
* @property int|null $call_duration_min
* @property int|null $call_duration_max
* @property array|null $groups
* @property array|null $playbook_categories
* @property array|null $deal_at_call_stages
* @property array|null $current_deal_stages
* @property array $recipients
* @property string|null $additional_prompt_input
* @property string|null $custom_name
* @property int|null $activity_search_id
* @property int|null $ask_anything_prompt_id
* @property Carbon|null $expires_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property-read \Jiminny\Models\Team $team
* @property-read \Jiminny\Models\Activity\Search|null $savedSearch
* @property-read \Jiminny\Models\AskAnything\AskAnythingPrompt|null $askAnythingPrompt
*/
class AutomatedReport extends Model
{
use RequiresUUID;
use SoftDeletes;
protected $table = 'automated_reports';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'team_id',
'type',
'status',
'frequency',
'from',
'to',
'deal_value_min',
'deal_value_max',
'call_types',
'media_types',
'call_duration_min',
'call_duration_max',
'groups',
'playbook_categories',
'deal_at_call_stages',
'current_deal_stages',
'recipients',
'jiminny_recipients',
'additional_prompt_input',
'custom_name',
'created_by',
'activity_search_id',
'ask_anything_prompt_id',
'expires_at',
];
protected $hidden = ['uuid'];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'status' => 'boolean',
'from' => 'datetime',
'to' => 'datetime',
'call_types' => 'array',
'media_types' => 'array',
'groups' => 'array',
'playbook_categories' => 'array',
'deal_at_call_stages' => 'array',
'current_deal_stages' => 'array',
'recipients' => 'array',
'jiminny_recipients' => 'array',
'expires_at' => 'date',
'deleted_at' => 'datetime',
];
}
/**
* Get the team that owns the automated report.
*/
public function team()
{
return $this->belongsTo(Team::class);
}
/**
*
* Get the user who created the report.
*/
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function savedSearch(): BelongsTo
{
return $this->belongsTo(Search::class, 'activity_search_id');
}
public function askAnythingPrompt(): BelongsTo
{
return $this->belongsTo(AskAnythingPrompt::class, 'ask_anything_prompt_id');
}
public function isAskJiminnyReport(): bool
{
return $this->getType() === AutomatedReportsService::TYPE_ASK_JIMINNY;
}
public function isExpired(): bool
{
$expiresAt = $this->getExpiresAt();
return $expiresAt !== null && $expiresAt->isPast();
}
public function getActivitySearchId(): ?int
{
return $this->getAttribute('activity_search_id');
}
public function getAskAnythingPromptId(): ?int
{
return $this->getAttribute('ask_anything_prompt_id');
}
public function getExpiresAt(): ?Carbon
{
return $this->getAttribute('expires_at');
}
public function getSavedSearch(): ?Search
{
return $this->getAttribute('savedSearch');
}
public function getAskAnythingPrompt(): ?AskAnythingPrompt
{
return $this->getAttribute('askAnythingPrompt');
}
/**
* Get the ID of the automated report.
*
* @return int
*/
public function getId(): int
{
return $this->getAttribute('id');
}
/**
* Get the UUID of the automated report.
*
* @return string
*/
public function getUuid(): string
{
return $this->getAttribute('id_string');
}
/**
* Get the team ID of the automated report.
*
* @return int
*/
public function getTeamId(): int
{
return $this->getAttribute('team_id');
}
/**
* Get the type of the automated report.
*
* @return string
*/
public function getType(): string
{
return $this->getAttribute('type');
}
/**
* Get the status of the automated report.
* True means active, false means inactive.
*
* @return bool
*/
public function getStatus(): bool
{
return $this->getAttribute('status');
}
/**
* Get the frequency of the automated report.
*
* @return string
*/
public function getFrequency(): string
{
return $this->getAttribute('frequency');
}
/**
* Get the from date of the automated report.
*
* @return Carbon|null
*/
public function getFrom(): ?Carbon
{
return $this->getAttribute('from');
}
/**
* Get the to date of the automated report.
*
* @return Carbon|null
*/
public function getTo(): ?Carbon
{
return $this->getAttribute('to');
}
/**
* Get the minimum deal value of the automated report.
*
* @return int|null
*/
public function getDealValueMin(): ?int
{
return $this->getAttribute('deal_value_min');
}
/**
* Get the maximum deal value of the automated report.
*
* @return int|null
*/
public function getDealValueMax(): ?int
{
return $this->getAttribute('deal_value_max');
}
/**
* Get the call types of the automated report.
*
* @return array
*/
public function getCallTypes(): array
{
return $this->getAttribute('call_types') ?? [];
}
public function getMediaTypes(): array
{
return $this->getAttribute('media_types') ?? [];
}
/**
* Get the minimum call duration of the automated report.
*
* @return int|null
*/
public function getCallDurationMin(): ?int
{
return $this->getAttribute('call_duration_min');
}
/**
* Get the maximum call duration of the automated report.
*
* @return int|null
*/
public function getCallDurationMax(): ?int
{
return $this->getAttribute('call_duration_max');
}
/**
* Get the groups of the automated report.
*
* @return array
*/
public function getGroups(): array
{
return $this->getAttribute('groups') ?? [];
}
/**
* Get the playbook categories of the automated report.
*
* @return array
*/
public function getPlaybookCategories(): array
{
return $this->getAttribute('playbook_categories') ?? [];
}
/**
* Get the deal at call stages of the automated report.
*
* @return array
*/
public function getDealAtCallStages(): array
{
return $this->getAttribute('deal_at_call_stages') ?? [];
}
/**
* Get the current deal stages of the automated report.
*
* @return array
*/
public function getCurrentDealStages(): array
{
return $this->getAttribute('current_deal_stages') ?? [];
}
/**
* Get the recipients of the automated report.
*
* @return array
*/
public function getRecipients(): array
{
return $this->getAttribute('recipients') ?? [];
}
/**
* Get the Jiminny's recipients of the automated report.
*
* @return array
*/
public function getJiminnyRecipients(): array
{
return $this->getAttribute('jiminny_recipients') ?? [];
}
/**
* Get the additional prompt input of the automated report.
*
* @return string|null
*/
public function getAdditionalPromptInput(): ?string
{
return $this->getAttribute('additional_prompt_input');
}
public function getCustomName(): ?string
{
return $this->getAttribute('custom_name');
}
/**
* Get the created at date of the automated report.
*
* @return Carbon
*/
public function getCreatedAt(): Carbon
{
return $this->getAttribute('created_at');
}
/**
* Get the updated at date of the automated report.
*
* @return Carbon
*/
public function getUpdatedAt(): Carbon
{
return $this->getAttribute('updated_at');
}
/**
* Get the deleted at date of the automated report.
*
* @return Carbon|null
*/
public function getDeletedAt(): ?Carbon
{
return $this->getAttribute('deleted_at');
}
public function getTeam(): Team
{
return $this->getAttribute('team');
}
public function getCreator(): ?User
{
return $this->getAttribute('creator');
}
}
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...
|
PhpStorm
|
faVsco.js – AutomatedReport.php
|
NULL
|
76705
|
|
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
8
1
7
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Traits\RequiresUUID;
/**
* Jiminny\Models\AutomatedReport
*
* @property int $id
* @property string $uuid
* @property int $team_id
* @property string $type
* @property bool $status
* @property string $frequency
* @property Carbon|null $from
* @property Carbon|null $to
* @property int|null $deal_value_min
* @property int|null $deal_value_max
* @property array $call_types
* @property array $media_types
* @property int|null $call_duration_min
* @property int|null $call_duration_max
* @property array|null $groups
* @property array|null $playbook_categories
* @property array|null $deal_at_call_stages
* @property array|null $current_deal_stages
* @property array $recipients
* @property string|null $additional_prompt_input
* @property string|null $custom_name
* @property int|null $activity_search_id
* @property int|null $ask_anything_prompt_id
* @property Carbon|null $expires_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property-read \Jiminny\Models\Team $team
* @property-read \Jiminny\Models\Activity\Search|null $savedSearch
* @property-read \Jiminny\Models\AskAnything\AskAnythingPrompt|null $askAnythingPrompt
*/
class AutomatedReport extends Model
{
use RequiresUUID;
use SoftDeletes;
protected $table = 'automated_reports';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'team_id',
'type',
'status',
'frequency',
'from',
'to',
'deal_value_min',
'deal_value_max',
'call_types',
'media_types',
'call_duration_min',
'call_duration_max',
'groups',
'playbook_categories',
'deal_at_call_stages',
'current_deal_stages',
'recipients',
'jiminny_recipients',
'additional_prompt_input',
'custom_name',
'created_by',
'activity_search_id',
'ask_anything_prompt_id',
'expires_at',
];
protected $hidden = ['uuid'];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'status' => 'boolean',
'from' => 'datetime',
'to' => 'datetime',
'call_types' => 'array',
'media_types' => 'array',
'groups' => 'array',
'playbook_categories' => 'array',
'deal_at_call_stages' => 'array',
'current_deal_stages' => 'array',
'recipients' => 'array',
'jiminny_recipients' => 'array',
'expires_at' => 'date',
'deleted_at' => 'datetime',
];
}
/**
* Get the team that owns the automated report.
*/
public function team()
{
return $this->belongsTo(Team::class);
}
/**
*
* Get the user who created the report.
*/
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function savedSearch(): BelongsTo
{
return $this->belongsTo(Search::class, 'activity_search_id');
}
public function askAnythingPrompt(): BelongsTo
{
return $this->belongsTo(AskAnythingPrompt::class, 'ask_anything_prompt_id');
}
public function isAskJiminnyReport(): bool
{
return $this->getType() === AutomatedReportsService::TYPE_ASK_JIMINNY;
}
public function isExpired(): bool
{
$expiresAt = $this->getExpiresAt();
return $expiresAt !== null && $expiresAt->isPast();
}
public function getActivitySearchId(): ?int
{
return $this->getAttribute('activity_search_id');
}
public function getAskAnythingPromptId(): ?int
{
return $this->getAttribute('ask_anything_prompt_id');
}
public function getExpiresAt(): ?Carbon
{
return $this->getAttribute('expires_at');
}
public function getSavedSearch(): ?Search
{
return $this->getAttribute('savedSearch');
}
public function getAskAnythingPrompt(): ?AskAnythingPrompt
{
return $this->getAttribute('askAnythingPrompt');
}
/**
* Get the ID of the automated report.
*
* @return int
*/
public function getId(): int
{
return $this->getAttribute('id');
}
/**
* Get the UUID of the automated report.
*
* @return string
*/
public function getUuid(): string
{
return $this->getAttribute('id_string');
}
/**
* Get the team ID of the automated report.
*
* @return int
*/
public function getTeamId(): int
{
return $this->getAttribute('team_id');
}
/**
* Get the type of the automated report.
*
* @return string
*/
public function getType(): string
{
return $this->getAttribute('type');
}
/**
* Get the status of the automated report.
* True means active, false means inactive.
*
* @return bool
*/
public function getStatus(): bool
{
return $this->getAttribute('status');
}
/**
* Get the frequency of the automated report.
*
* @return string
*/
public function getFrequency(): string
{
return $this->getAttribute('frequency');
}
/**
* Get the from date of the automated report.
*
* @return Carbon|null
*/
public function getFrom(): ?Carbon
{
return $this->getAttribute('from');
}
/**
* Get the to date of the automated report.
*
* @return Carbon|null
*/
public function getTo(): ?Carbon
{
return $this->getAttribute('to');
}
/**
* Get the minimum deal value of the automated report.
*
* @return int|null
*/
public function getDealValueMin(): ?int
{
return $this->getAttribute('deal_value_min');
}
/**
* Get the maximum deal value of the automated report.
*
* @return int|null
*/
public function getDealValueMax(): ?int
{
return $this->getAttribute('deal_value_max');
}
/**
* Get the call types of the automated report.
*
* @return array
*/
public function getCallTypes(): array
{
return $this->getAttribute('call_types') ?? [];
}
public function getMediaTypes(): array
{
return $this->getAttribute('media_types') ?? [];
}
/**
* Get the minimum call duration of the automated report.
*
* @return int|null
*/
public function getCallDurationMin(): ?int
{
return $this->getAttribute('call_duration_min');
}
/**
* Get the maximum call duration of the automated report.
*
* @return int|null
*/
public function getCallDurationMax(): ?int
{
return $this->getAttribute('call_duration_max');
}
/**
* Get the groups of the automated report.
*
* @return array
*/
public function getGroups(): array
{
return $this->getAttribute('groups') ?? [];
}
/**
* Get the playbook categories of the automated report.
*
* @return array
*/
public function getPlaybookCategories(): array
{
return $this->getAttribute('playbook_categories') ?? [];
}
/**
* Get the deal at call stages of the automated report.
*
* @return array
*/
public function getDealAtCallStages(): array
{
return $this->getAttribute('deal_at_call_stages') ?? [];
}
/**
* Get the current deal stages of the automated report.
*
* @return array
*/
public function getCurrentDealStages(): array
{
return $this->getAttribute('current_deal_stages') ?? [];
}
/**
* Get the recipients of the automated report.
*
* @return array
*/
public function getRecipients(): array
{
return $this->getAttribute('recipients') ?? [];
}
/**
* Get the Jiminny's recipients of the automated report.
*
* @return array
*/
public function getJiminnyRecipients(): array
{
return $this->getAttribute('jiminny_recipients') ?? [];
}
/**
* Get the additional prompt input of the automated report.
*
* @return string|null
*/
public function getAdditionalPromptInput(): ?string
{
return $this->getAttribute('additional_prompt_input');
}
public function getCustomName(): ?string
{
return $this->getAttribute('custom_name');
}
/**
* Get the created at date of the automated report.
*
* @return Carbon
*/
public function getCreatedAt(): Carbon
{
return $this->getAttribute('created_at');
}
/**
* Get the updated at date of the automated report.
*
* @return Carbon
*/
public function getUpdatedAt(): Carbon
{
return $this->getAttribute('updated_at');
}
/**
* Get the deleted at date of the automated report.
*
* @return Carbon|null
*/
public function getDeletedAt(): ?Carbon
{
return $this->getAttribute('deleted_at');
}
public function getTeam(): Team
{
return $this->getAttribute('team');
}
public function getCreator(): ?User
{
return $this->getAttribute('creator');
}
}
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...
|
PhpStorm
|
faVsco.js – AutomatedReport.php
|
NULL
|
76706
|
|
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
8
1
7
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Traits\RequiresUUID;
/**
* Jiminny\Models\AutomatedReport
*
* @property int $id
* @property string $uuid
* @property int $team_id
* @property string $type
* @property bool $status
* @property string $frequency
* @property Carbon|null $from
* @property Carbon|null $to
* @property int|null $deal_value_min
* @property int|null $deal_value_max
* @property array $call_types
* @property array $media_types
* @property int|null $call_duration_min
* @property int|null $call_duration_max
* @property array|null $groups
* @property array|null $playbook_categories
* @property array|null $deal_at_call_stages
* @property array|null $current_deal_stages
* @property array $recipients
* @property string|null $additional_prompt_input
* @property string|null $custom_name
* @property int|null $activity_search_id
* @property int|null $ask_anything_prompt_id
* @property Carbon|null $expires_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property-read \Jiminny\Models\Team $team
* @property-read \Jiminny\Models\Activity\Search|null $savedSearch
* @property-read \Jiminny\Models\AskAnything\AskAnythingPrompt|null $askAnythingPrompt
*/
class AutomatedReport extends Model
{
use RequiresUUID;
use SoftDeletes;
protected $table = 'automated_reports';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'team_id',
'type',
'status',
'frequency',
'from',
'to',
'deal_value_min',
'deal_value_max',
'call_types',
'media_types',
'call_duration_min',
'call_duration_max',
'groups',
'playbook_categories',
'deal_at_call_stages',
'current_deal_stages',
'recipients',
'jiminny_recipients',
'additional_prompt_input',
'custom_name',
'created_by',
'activity_search_id',
'ask_anything_prompt_id',
'expires_at',
];
protected $hidden = ['uuid'];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'status' => 'boolean',
'from' => 'datetime',
'to' => 'datetime',
'call_types' => 'array',
'media_types' => 'array',
'groups' => 'array',
'playbook_categories' => 'array',
'deal_at_call_stages' => 'array',
'current_deal_stages' => 'array',
'recipients' => 'array',
'jiminny_recipients' => 'array',
'expires_at' => 'date',
'deleted_at' => 'datetime',
];
}
/**
* Get the team that owns the automated report.
*/
public function team()
{
return $this->belongsTo(Team::class);
}
/**
*
* Get the user who created the report.
*/
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function savedSearch(): BelongsTo
{
return $this->belongsTo(Search::class, 'activity_search_id');
}
public function askAnythingPrompt(): BelongsTo
{
return $this->belongsTo(AskAnythingPrompt::class, 'ask_anything_prompt_id');
}
public function isAskJiminnyReport(): bool
{
return $this->getType() === AutomatedReportsService::TYPE_ASK_JIMINNY;
}
public function isExpired(): bool
{
$expiresAt = $this->getExpiresAt();
return $expiresAt !== null && $expiresAt->isPast();
}
public function getActivitySearchId(): ?int
{
return $this->getAttribute('activity_search_id');
}
public function getAskAnythingPromptId(): ?int
{
return $this->getAttribute('ask_anything_prompt_id');
}
public function getExpiresAt(): ?Carbon
{
return $this->getAttribute('expires_at');
}
public function getSavedSearch(): ?Search
{
return $this->getAttribute('savedSearch');
}
public function getAskAnythingPrompt(): ?AskAnythingPrompt
{
return $this->getAttribute('askAnythingPrompt');
}
/**
* Get the ID of the automated report.
*
* @return int
*/
public function getId(): int
{
return $this->getAttribute('id');
}
/**
* Get the UUID of the automated report.
*
* @return string
*/
public function getUuid(): string
{
return $this->getAttribute('id_string');
}
/**
* Get the team ID of the automated report.
*
* @return int
*/
public function getTeamId(): int
{
return $this->getAttribute('team_id');
}
/**
* Get the type of the automated report.
*
* @return string
*/
public function getType(): string
{
return $this->getAttribute('type');
}
/**
* Get the status of the automated report.
* True means active, false means inactive.
*
* @return bool
*/
public function getStatus(): bool
{
return $this->getAttribute('status');
}
/**
* Get the frequency of the automated report.
*
* @return string
*/
public function getFrequency(): string
{
return $this->getAttribute('frequency');
}
/**
* Get the from date of the automated report.
*
* @return Carbon|null
*/
public function getFrom(): ?Carbon
{
return $this->getAttribute('from');
}
/**
* Get the to date of the automated report.
*
* @return Carbon|null
*/
public function getTo(): ?Carbon
{
return $this->getAttribute('to');
}
/**
* Get the minimum deal value of the automated report.
*
* @return int|null
*/
public function getDealValueMin(): ?int
{
return $this->getAttribute('deal_value_min');
}
/**
* Get the maximum deal value of the automated report.
*
* @return int|null
*/
public function getDealValueMax(): ?int
{
return $this->getAttribute('deal_value_max');
}
/**
* Get the call types of the automated report.
*
* @return array
*/
public function getCallTypes(): array
{
return $this->getAttribute('call_types') ?? [];
}
public function getMediaTypes(): array
{
return $this->getAttribute('media_types') ?? [];
}
/**
* Get the minimum call duration of the automated report.
*
* @return int|null
*/
public function getCallDurationMin(): ?int
{
return $this->getAttribute('call_duration_min');
}
/**
* Get the maximum call duration of the automated report.
*
* @return int|null
*/
public function getCallDurationMax(): ?int
{
return $this->getAttribute('call_duration_max');
}
/**
* Get the groups of the automated report.
*
* @return array
*/
public function getGroups(): array
{
return $this->getAttribute('groups') ?? [];
}
/**
* Get the playbook categories of the automated report.
*
* @return array
*/
public function getPlaybookCategories(): array
{
return $this->getAttribute('playbook_categories') ?? [];
}
/**
* Get the deal at call stages of the automated report.
*
* @return array
*/
public function getDealAtCallStages(): array
{
return $this->getAttribute('deal_at_call_stages') ?? [];
}
/**
* Get the current deal stages of the automated report.
*
* @return array
*/
public function getCurrentDealStages(): array
{
return $this->getAttribute('current_deal_stages') ?? [];
}
/**
* Get the recipients of the automated report.
*
* @return array
*/
public function getRecipients(): array
{
return $this->getAttribute('recipients') ?? [];
}
/**
* Get the Jiminny's recipients of the automated report.
*
* @return array
*/
public function getJiminnyRecipients(): array
{
return $this->getAttribute('jiminny_recipients') ?? [];
}
/**
* Get the additional prompt input of the automated report.
*
* @return string|null
*/
public function getAdditionalPromptInput(): ?string
{
return $this->getAttribute('additional_prompt_input');
}
public function getCustomName(): ?string
{
return $this->getAttribute('custom_name');
}
/**
* Get the created at date of the automated report.
*
* @return Carbon
*/
public function getCreatedAt(): Carbon
{
return $this->getAttribute('created_at');
}
/**
* Get the updated at date of the automated report.
*
* @return Carbon
*/
public function getUpdatedAt(): Carbon
{
return $this->getAttribute('updated_at');
}
/**
* Get the deleted at date of the automated report.
*
* @return Carbon|null
*/
public function getDeletedAt(): ?Carbon
{
return $this->getAttribute('deleted_at');
}
public function getTeam(): Team
{
return $this->getAttribute('team');
}
public function getCreator(): ?User
{
return $this->getAttribute('creator');
}
}
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...
|
PhpStorm
|
faVsco.js – AutomatedReport.php
|
NULL
|
76707
|
|
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
NULL
|
76708
|
|
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
NULL
|
76709
|
|
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
4
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\AutomatedReports;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\Queue\Constants;
use Jiminny\Exceptions\FileNotFoundException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Jobs\JobDispatcherInterface;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Throwable;
class SendReportJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[Send Report]';
/**
* The number of times the job may be attempted.
*
* @var int
*/
public int $tries = 2;
/**
* The maximum number of seconds the job should be allowed to run.
*
* @var int
*/
public int $timeout = 120;
/**
* Create a new job instance.
*
* @param string $reportUuid The UUID of the report to send
*/
public function __construct(private readonly string $reportUuid)
{
$this->onQueue(Constants::QUEUE_ANALYTICS_LOW);
}
public function handle(
LoggerInterface $logger,
JobDispatcherInterface $jobDispatcher,
AutomatedReportsService $automatedReportsService
): void {
try {
$report = $automatedReportsService->getReportResult($this->reportUuid);
} catch (Throwable $e) {
$logger->error(self::LOG_PREFIX . ' Error getting report result', [
'uuid' => $this->reportUuid,
'exception' => $e->getMessage(),
]);
throw $e;
}
$logger->info(self::LOG_PREFIX . ' Processing report', [
'uuid' => $this->reportUuid,
'status' => $report->getStatusLabel(),
]);
$s3Path = $automatedReportsService->getMediaPath($report);
// Verify the file exists in S3
if (! Storage::disk('client-data-cloud')->exists($s3Path)) {
$logger->error(self::LOG_PREFIX . ' Report file not found in S3', [
'uuid' => $this->reportUuid,
's3Path' => $s3Path,
]);
throw new FileNotFoundException('Report file not found in S3');
}
try {
$validRecipients = $automatedReportsService->getValidRecipientUsers(
$report->getReport(),
includeJiminny: true,
);
if (empty($validRecipients)) {
$logger->error(self::LOG_PREFIX . ' No valid recipients found', [
'uuid' => $this->reportUuid,
]);
throw new RuntimeException("No recipients found for report: {$this->reportUuid}");
}
// Get report metadata (same for all recipients)
$fileName = $automatedReportsService->getReportFileName($report);
$typeName = $report->getReport()->getCustomName()
?? $automatedReportsService->getReportTypeName($report);
$teamsName = $automatedReportsService->getReportTeamsName($report);
$periodName = $automatedReportsService->getReportPeriodName($report);
$isAskJiminny = $report->getReport()->isAskJiminnyReport();
// Track how many jobs were dispatched
$dispatchCount = 0;
// Dispatch a job for each recipient
foreach ($validRecipients as $recipient) {
$jobDispatcher->dispatch(
new SendReportMailJob(
reportUuid: $this->reportUuid,
s3Path: $s3Path,
recipientEmail: $recipient['email'],
recipientName: $recipient['name'] ?? null,
fileName: $fileName,
typeName: $typeName,
teamsName: $teamsName,
periodName: $periodName,
isAskJiminny: $isAskJiminny,
)
);
$dispatchCount++;
$logger->info(self::LOG_PREFIX . ' Dispatched mail job', [
'uuid' => $this->reportUuid,
'email' => $recipient['email'],
]);
}
$logger->info(self::LOG_PREFIX . ' Successfully dispatched mail jobs', [
'uuid' => $this->reportUuid,
'count' => $dispatchCount,
]);
} catch (Throwable $e) {
$logger->error(self::LOG_PREFIX . ' Error processing report', [
'uuid' => $this->reportUuid,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// Get current attempt (1-indexed)
$attempt = $this->attempts();
if ($attempt < $this->tries) {
$delay = 30 * (2 ** ($attempt - 1)); // Exponential backoff: 30s, 60s
$logger->info(self::LOG_PREFIX . ' retry job, attempts: ' . $attempt, [
'delay' => $delay,
]);
$this->release($delay);
} else {
$this->fail($e);
}
}
}
/**
* Get a unique ID for the job.
*
* @return string
*/
public function uniqueId(): string
{
return $this->reportUuid;
}
}
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...
|
PhpStorm
|
faVsco.js – SendReportJob.php
|
NULL
|
76710
|
|
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
4
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\AutomatedReports;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\Queue\Constants;
use Jiminny\Exceptions\FileNotFoundException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Jobs\JobDispatcherInterface;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Throwable;
class SendReportJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[Send Report]';
/**
* The number of times the job may be attempted.
*
* @var int
*/
public int $tries = 2;
/**
* The maximum number of seconds the job should be allowed to run.
*
* @var int
*/
public int $timeout = 120;
/**
* Create a new job instance.
*
* @param string $reportUuid The UUID of the report to send
*/
public function __construct(private readonly string $reportUuid)
{
$this->onQueue(Constants::QUEUE_ANALYTICS_LOW);
}
public function handle(
LoggerInterface $logger,
JobDispatcherInterface $jobDispatcher,
AutomatedReportsService $automatedReportsService
): void {
try {
$report = $automatedReportsService->getReportResult($this->reportUuid);
} catch (Throwable $e) {
$logger->error(self::LOG_PREFIX . ' Error getting report result', [
'uuid' => $this->reportUuid,
'exception' => $e->getMessage(),
]);
throw $e;
}
$logger->info(self::LOG_PREFIX . ' Processing report', [
'uuid' => $this->reportUuid,
'status' => $report->getStatusLabel(),
]);
$s3Path = $automatedReportsService->getMediaPath($report);
// Verify the file exists in S3
if (! Storage::disk('client-data-cloud')->exists($s3Path)) {
$logger->error(self::LOG_PREFIX . ' Report file not found in S3', [
'uuid' => $this->reportUuid,
's3Path' => $s3Path,
]);
throw new FileNotFoundException('Report file not found in S3');
}
try {
$validRecipients = $automatedReportsService->getValidRecipientUsers(
$report->getReport(),
includeJiminny: true,
);
if (empty($validRecipients)) {
$logger->error(self::LOG_PREFIX . ' No valid recipients found', [
'uuid' => $this->reportUuid,
]);
throw new RuntimeException("No recipients found for report: {$this->reportUuid}");
}
// Get report metadata (same for all recipients)
$fileName = $automatedReportsService->getReportFileName($report);
$typeName = $report->getReport()->getCustomName()
?? $automatedReportsService->getReportTypeName($report);
$teamsName = $automatedReportsService->getReportTeamsName($report);
$periodName = $automatedReportsService->getReportPeriodName($report);
$isAskJiminny = $report->getReport()->isAskJiminnyReport();
// Track how many jobs were dispatched
$dispatchCount = 0;
// Dispatch a job for each recipient
foreach ($validRecipients as $recipient) {
$jobDispatcher->dispatch(
new SendReportMailJob(
reportUuid: $this->reportUuid,
s3Path: $s3Path,
recipientEmail: $recipient['email'],
recipientName: $recipient['name'] ?? null,
fileName: $fileName,
typeName: $typeName,
teamsName: $teamsName,
periodName: $periodName,
isAskJiminny: $isAskJiminny,
)
);
$dispatchCount++;
$logger->info(self::LOG_PREFIX . ' Dispatched mail job', [
'uuid' => $this->reportUuid,
'email' => $recipient['email'],
]);
}
$logger->info(self::LOG_PREFIX . ' Successfully dispatched mail jobs', [
'uuid' => $this->reportUuid,
'count' => $dispatchCount,
]);
} catch (Throwable $e) {
$logger->error(self::LOG_PREFIX . ' Error processing report', [
'uuid' => $this->reportUuid,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// Get current attempt (1-indexed)
$attempt = $this->attempts();
if ($attempt < $this->tries) {
$delay = 30 * (2 ** ($attempt - 1)); // Exponential backoff: 30s, 60s
$logger->info(self::LOG_PREFIX . ' retry job, attempts: ' . $attempt, [
'delay' => $delay,
]);
$this->release($delay);
} else {
$this->fail($e);
}
}
}
/**
* Get a unique ID for the job.
*
* @return string
*/
public function uniqueId(): string
{
return $this->reportUuid;
}
}
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...
|
PhpStorm
|
faVsco.js – SendReportJob.php
|
NULL
|
76711
|
|
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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Jobs\AutomatedReports;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\Queue\Constants;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Mail\Reports\ReportWithAttachment;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Sentry\Laravel\Facade as Sentry;
use Throwable;
class SendReportMailJob implements ShouldBeUnique, ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[Send Report Mail]';
/**
* The number of times the job may be attempted.
*
* @var int
*/
public int $tries = 3;
/**
* The number of seconds to wait before retrying the job.
*
* @var array<int, int>
*/
public array $backoff = [60, 300, 600];
/**
* The maximum number of seconds the job should be allowed to run.
*
* @var int
*/
public int $timeout = 120;
/**
* Create a new job instance.
*/
public function __construct(
private readonly string $reportUuid,
private readonly string $s3Path,
private readonly string $recipientEmail,
private readonly ?string $recipientName,
private readonly string $fileName,
private readonly string $typeName,
private readonly string $teamsName,
private readonly string $periodName,
private readonly bool $isAskJiminny = false,
) {
$this->onQueue(Constants::QUEUE_ANALYTICS_LOW);
}
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return $this->reportUuid . '_' . $this->recipientEmail;
}
/**
* Execute the job.
*/
public function handle(
LoggerInterface $logger,
AutomatedReportsService $automatedReportsService,
UrlGenerator $urlGenerator,
): void {
// Check if the file exists in S3
$disk = Storage::disk('client-data-cloud');
if (! $disk->exists($this->s3Path)) {
$logger->error(self::LOG_PREFIX . ' Report file not found in S3', [
'uuid' => $this->reportUuid,
'path' => $this->s3Path,
'email' => $this->recipientEmail,
]);
throw new RuntimeException('Report file not found in S3');
}
$report = $automatedReportsService->getReportResult($this->reportUuid);
$reportsPageUrl = $urlGenerator->route('ai.reports.show');
$suffix = $automatedReportsService->getMailSubjectSuffix($report);
$mailSubject = $this->isAskJiminny
? 'Your ' . $this->typeName . ' ' . $suffix . ' is ready'
: 'Your ' . $this->typeName . ' ' . $suffix . ' for ' . $this->periodName . ' is ready';
try {
// Create mailable with S3 path
$mailable = new ReportWithAttachment(
s3Path: $this->s3Path,
fileName: $this->fileName,
typeName: $this->typeName,
teamsName: $this->teamsName,
periodName: $this->periodName,
reportsPageUrl: $reportsPageUrl,
mailSubject: $mailSubject,
mediaTypeMetadata: $automatedReportsService->getMediaTypeMetadata($report),
isAskJiminny: $this->isAskJiminny,
);
// Send the email
Mail::mailer('postmark')
->to($this->recipientEmail)
->send($mailable);
$logger->info(self::LOG_PREFIX . ' Email sent', [
'uuid' => $this->reportUuid,
'email' => $this->recipientEmail,
'recipientName' => $this->recipientName,
]);
// Update report status if this is the first successfully sent email
// add cache lock to avoid race conditions
if ($report->status !== AutomatedReportResult::STATUS_SENT) {
Cache::lock("report-status-{$this->reportUuid}", 5)
->block(2, function () use ($report) {
$report->update([
'status' => AutomatedReportResult::STATUS_SENT,
'sent_at' => now(),
]);
});
}
} catch (Throwable $e) {
$logger->error(self::LOG_PREFIX . ' Error sending email', [
'uuid' => $this->reportUuid,
'email' => $this->recipientEmail,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// Report to Sentry
Sentry::captureException($e);
// Determine if we should retry or fail
if ($this->attempts() < $this->tries) {
$backoffIndex = min($this->attempts() - 1, count($this->backoff) - 1);
$this->release($this->backoff[$backoffIndex]);
} else {
$this->fail($e);
}
}
}
}
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...
|
PhpStorm
|
faVsco.js – SendReportMailJob.php
|
NULL
|
76712
|
|
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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Jobs\AutomatedReports;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\Queue\Constants;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Mail\Reports\ReportWithAttachment;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Sentry\Laravel\Facade as Sentry;
use Throwable;
class SendReportMailJob implements ShouldBeUnique, ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
/**
* Log prefix for all log messages
*/
private const string LOG_PREFIX = '[Send Report Mail]';
/**
* The number of times the job may be attempted.
*
* @var int
*/
public int $tries = 3;
/**
* The number of seconds to wait before retrying the job.
*
* @var array<int, int>
*/
public array $backoff = [60, 300, 600];
/**
* The maximum number of seconds the job should be allowed to run.
*
* @var int
*/
public int $timeout = 120;
/**
* Create a new job instance.
*/
public function __construct(
private readonly string $reportUuid,
private readonly string $s3Path,
private readonly string $recipientEmail,
private readonly ?string $recipientName,
private readonly string $fileName,
private readonly string $typeName,
private readonly string $teamsName,
private readonly string $periodName,
private readonly bool $isAskJiminny = false,
) {
$this->onQueue(Constants::QUEUE_ANALYTICS_LOW);
}
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return $this->reportUuid . '_' . $this->recipientEmail;
}
/**
* Execute the job.
*/
public function handle(
LoggerInterface $logger,
AutomatedReportsService $automatedReportsService,
UrlGenerator $urlGenerator,
): void {
// Check if the file exists in S3
$disk = Storage::disk('client-data-cloud');
if (! $disk->exists($this->s3Path)) {
$logger->error(self::LOG_PREFIX . ' Report file not found in S3', [
'uuid' => $this->reportUuid,
'path' => $this->s3Path,
'email' => $this->recipientEmail,
]);
throw new RuntimeException('Report file not found in S3');
}
$report = $automatedReportsService->getReportResult($this->reportUuid);
$reportsPageUrl = $urlGenerator->route('ai.reports.show');
$suffix = $automatedReportsService->getMailSubjectSuffix($report);
$mailSubject = $this->isAskJiminny
? 'Your ' . $this->typeName . ' ' . $suffix . ' is ready'
: 'Your ' . $this->typeName . ' ' . $suffix . ' for ' . $this->periodName . ' is ready';
try {
// Create mailable with S3 path
$mailable = new ReportWithAttachment(
s3Path: $this->s3Path,
fileName: $this->fileName,
typeName: $this->typeName,
teamsName: $this->teamsName,
periodName: $this->periodName,
reportsPageUrl: $reportsPageUrl,
mailSubject: $mailSubject,
mediaTypeMetadata: $automatedReportsService->getMediaTypeMetadata($report),
isAskJiminny: $this->isAskJiminny,
);
// Send the email
Mail::mailer('postmark')
->to($this->recipientEmail)
->send($mailable);
$logger->info(self::LOG_PREFIX . ' Email sent', [
'uuid' => $this->reportUuid,
'email' => $this->recipientEmail,
'recipientName' => $this->recipientName,
]);
// Update report status if this is the first successfully sent email
// add cache lock to avoid race conditions
if ($report->status !== AutomatedReportResult::STATUS_SENT) {
Cache::lock("report-status-{$this->reportUuid}", 5)
->block(2, function () use ($report) {
$report->update([
'status' => AutomatedReportResult::STATUS_SENT,
'sent_at' => now(),
]);
});
}
} catch (Throwable $e) {
$logger->error(self::LOG_PREFIX . ' Error sending email', [
'uuid' => $this->reportUuid,
'email' => $this->recipientEmail,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// Report to Sentry
Sentry::captureException($e);
// Determine if we should retry or fail
if ($this->attempts() < $this->tries) {
$backoffIndex = min($this->attempts() - 1, count($this->backoff) - 1);
$this->release($this->backoff[$backoffIndex]);
} else {
$this->fail($e);
}
}
}
}
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...
|
PhpStorm
|
faVsco.js – SendReportMailJob.php
|
NULL
|
76713
|
|
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
Analyzing…
<?php
namespace Jiminny\Console;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Jiminny\Component\Acl\RemoveExpiredRoleChangeEventsCommand;
use Jiminny\Component\ActionItems\Commands\SendActionItemsCommand;
use Jiminny\Component\AiActivityType\Commands\AutodetectAiActivityTypeCommand;
use Jiminny\Component\AskJiminnyAi\Commands\ProphetAnalyzeClosedDealsCommand;
use Jiminny\Component\Cache\Constants;
use Jiminny\Component\DealInsights\Commands\SendDealsUpdateCommand;
use Jiminny\Component\MediaPipeline\Command\MediaPipelineRestartCommand;
use Jiminny\Component\MediaPipeline\Command\ReportActivityProcessingTimeToDatadogCommand;
use Jiminny\Component\MediaPipeline\Command\ReportProcessingStatesToDatadogCommand;
use Jiminny\Component\Transcription\Commands\OverrideTranscriptionLocaleCommand;
use Jiminny\Component\Transcription\Commands\RetryFailedTranscriptionsCommand;
use Jiminny\Component\Transcription\Commands\RetryStuckTranscriptionsCommand;
use Jiminny\Console\Commands\Activities\ActivitiesMatchCrmCommand;
use Jiminny\Console\Commands\Activities\AutologOldActivitiesCommand;
use Jiminny\Console\Commands\Activities\DeleteActivitiesForChurnedTeamsCommand;
use Jiminny\Console\Commands\Activities\DeleteActivitiesForRetentionTeamsCommand;
use Jiminny\Console\Commands\Activities\DownloadMissingTrackCommand;
use Jiminny\Console\Commands\Activities\FixActivitiesOpportunity;
use Jiminny\Console\Commands\Activities\HardDeleteActivitiesForChurnedTeamsCommand;
use Jiminny\Console\Commands\Activities\HardDeleteActivitiesTeamsCommand;
use Jiminny\Console\Commands\Activities\ReassignTranscriptCommand;
use Jiminny\Console\Commands\Activities\ReindexRecentActivitiesCommand;
use Jiminny\Console\Commands\Activities\RetryProspectSummaryCommand;
use Jiminny\Console\Commands\Calendars\Events\CalendarEventDeleteCancelledCommand;
use Jiminny\Console\Commands\Calendars\Events\CalendarEventDeletePastCommand;
use Jiminny\Console\Commands\Crm\CleanDuplicateFieldDataCommand;
use Jiminny\Console\Commands\Crm\Hubspot\ProcessMergedObjectsCommand;
use Jiminny\Console\Commands\Crm\Hubspot\RestoreDealAssociationsCommand;
use Jiminny\Console\Commands\Crm\ProcessHubspotObjectsSyncBatches;
use Jiminny\Console\Commands\Crm\PurgeDeletedOpportunitiesCommand;
use Jiminny\Console\Commands\Crm\Hubspot\ListJournalWebhookSubscriptionsCommand;
use Jiminny\Console\Commands\Crm\Hubspot\SetupJournalDealWebhookSubscriptionsCommand;
use Jiminny\Console\Commands\Crm\SyncHubspotActiveDeals;
use Jiminny\Console\Commands\Crm\SyncOpportunitiesMissingFieldDataCommand;
use Jiminny\Console\Commands\DeleteOldAiCrmNotesCommand;
use Jiminny\Console\Commands\DeleteS3LeftoversCommand;
use Jiminny\Console\Commands\DiarizeViaAiParticipantIdentificationCommand;
use Jiminny\Console\Commands\Elasticsearch\DeleteEmailDocumentsCommand;
use Jiminny\Console\Commands\Elasticsearch\RemoveGhostParticipantsCommand;
use Jiminny\Console\Commands\FlushRolesPermissionsCache;
use Jiminny\Console\Commands\GenerateInternalWebhookToken;
use Jiminny\Console\Commands\HubspotJournalPollingCommand;
use Jiminny\Console\Commands\HubspotWebhookServiceCommand;
use Jiminny\Console\Commands\Livestream\StopHangingLivestreamsCommand;
use Jiminny\Console\Commands\Mailboxes\DeleteEmailMessagesWithoutActivityCommand;
use Jiminny\Console\Commands\Mailboxes\DeleteInboxEmailsCommand;
use Jiminny\Console\Commands\PurgeSoftDeletedOpportunitiesCommand;
use Jiminny\Console\Commands\PurgeSyncBatchesCommand;
use Jiminny\Console\Commands\RemoveDeleteMarkersCommand;
use Jiminny\Console\Commands\RemoveExpiredNudgesCommand;
use Jiminny\Console\Commands\RemoveUnusedParticipantSpeechesCommand;
use Jiminny\Console\Commands\Reports\AutomatedReportsRetentionPolicyCommand;
use Jiminny\Console\Commands\Reports\DeleteReportCommand;
use Jiminny\Console\Commands\RestoreActivityCrmProviderIdCommand;
use Jiminny\Console\Commands\RestoreActivityTypeCommand;
use Jiminny\Console\Commands\Slack\SyncSlackUserCommand;
use Jiminny\Console\Commands\Teams\SyncTeamUsersCommand;
use Jiminny\Console\Commands\Teams\TeamDeleteCommand;
use Jiminny\Console\Commands\Teams\TeamsDeleteDeactivatedCommand;
use Jiminny\Console\Commands\Teams\TeamsDeleteRetentionCommand;
use Jiminny\Console\Commands\Teams\TeamSettingPutCommand;
use Jiminny\Console\Commands\Teams\UpdateTeamsCommand;
use Jiminny\Console\Commands\Tracks\DeleteUnusedTracksCommand;
use Jiminny\Console\Commands\Tracks\RestoreTracksCommand;
use Jiminny\Console\Commands\Transcription\DeleteOldTranscriptionsCommand;
use Jiminny\Console\Commands\Transcription\UpdateOldTranscriptionModelLocalesCommand;
use Jiminny\Console\Commands\Twilio\DeleteChurnedSubAccounts;
use Jiminny\Console\Commands\Twilio\DeletePredefinedSubAccounts;
use Jiminny\Console\Commands\Twilio\ReleaseNumbersCommand;
use Jiminny\Jobs\Activity\SyncActivity;
use Jiminny\Models\Activity;
use Jiminny\Models\InboxEmail;
use Jiminny\Services\RecallAI\Commands\ImportRegionMeetingCommand;
use Jiminny\Services\RecallAI\Commands\ScheduleBotCommand;
class Kernel extends ConsoleKernel
{
use ConfirmableTrait;
/**
* The Artisan commands provided by your application.
*
* @var string[]
*/
protected $commands = [
Commands\GeckoExport\GeckoExportTranscriptCommand::class,
Commands\GeckoExport\GeckoExportTranscriptionCommand::class,
Commands\GeckoExport\GeckoExportParticipantSpeechesCommand::class,
Commands\Activities\DeleteForCoachesCommand::class,
ReindexRecentActivitiesCommand::class,
Commands\Crm\BullhornPingCommand::class,
Commands\Crm\BullhornSessionCommand::class,
Commands\Crm\BullhornSearchCommand::class,
Commands\PlaybackThemes\TopicsConsolidateCommand::class,
Commands\PlaybackThemes\PlaybackThemesCopyCommand::class,
Commands\PlaybackThemes\AssignTopicsUsedBySingleTeamCommand::class,
Commands\PlaybackThemes\PlaybackThemesMigrateToVersionsCommand::class,
Commands\Vocabulary\VocabularyCopyCommand::class,
Commands\Transcription\TranscriptionPrintRaw::class,
Commands\Migrate\JiminnyMigratePopulateActivitySourceCommand::class,
Commands\EngagementStats\JiminnyEngagementStatsExplainCommand::class,
Commands\EngagementStatsRegenerateCommand::class,
Commands\Analytics\NumberOfActivitiesPerActivityTypeCommand::class,
Commands\Elasticsearch\MappingRunCommand::class,
Commands\Elasticsearch\MappingInstallCommand::class,
Commands\Elasticsearch\UpdateEsMappingSettingsCommand::class,
Commands\Analytics\TranscriptionWordMatchCommand::class,
Commands\JiminnyCacheClearCommand::class,
Commands\Transcription\TranscriptionSearchCommand::class,
RetryStuckTranscriptionsCommand::class,
RetryFailedTranscriptionsCommand::class,
Commands\JiminnyDebugCommand::class,
Commands\Calendars\SyncCalendars::class,
Commands\Calendars\SyncDeletedEvents::class,
Commands\Twilio\FetchMetrics::class,
Commands\Twilio\FetchEvents::class,
Commands\Twilio\FetchSummary::class,
Commands\Twilio\SyncZoneAccess::class,
Commands\DatabaseTableCount::class,
Commands\PurgeConferences::class,
Commands\ResetElasticSearch::class,
Commands\CreateDatabaseUsers::class,
Commands\Activities\NotifyNotLogged::class,
Commands\Crm\SyncTeamMetadata::class,
Commands\Crm\SyncProfileMetadata::class,
Commands\Crm\SyncContact::class,
Commands\Crm\SyncObjects::class,
Commands\Crm\SyncHubspotObjects::class,
Commands\Crm\SyncAccount::class,
Commands\Crm\ResetGovernorLimits::class,
Commands\Crm\ManageSyncStrategyCommand::class,
Commands\ImportRecording::class,
Commands\TrackImported::class,
Commands\Twilio\RecoverTwilioTracksCommand::class,
Commands\Crm\SetupLayouts::class,
Commands\Tracks\SyncTwilioTracks::class,
Commands\Activities\StatusCount::class,
Commands\Mailboxes\TextRelay\WatchMailboxEvents::class,
Commands\Mailboxes\InboxCreate::class,
Commands\Mailboxes\InboxSync::class,
Commands\Mailboxes\BatchCreate::class,
Commands\Mailboxes\BatchProcess::class,
Commands\Mailboxes\InboxPurge::class,
Commands\Mailboxes\BatchRetryFailed::class,
Commands\Mailboxes\BatchFailStalled::class,
Commands\Mailboxes\SkipListsRefresh::class,
Commands\Mailboxes\SkipListsDump::class,
Commands\Mailboxes\TextRelay\SyncMailbox::class,
Commands\Mailboxes\DeleteInboxEmailsCommand::class,
Commands\Mailboxes\DeleteEmailMessagesCommand::class,
DeleteEmailMessagesWithoutActivityCommand::class,
Commands\Tracks\CheckIntegrity::class,
Commands\Twilio\RemoteLifecycle::class,
Commands\Twilio\SyncNumbers::class,
Commands\Crm\SetupActivityTypeForFollowUp::class,
Commands\Activities\CheckPlayable::class,
Commands\Activities\ActivityDeleteCommand::class,
Commands\Activities\Copy::class,
Commands\Activities\ActivityHardDeleteCommand::class,
Commands\Reports\Team::class,
Commands\Reports\GenerateMarketingReport::class,
Commands\Reports\AutomatedReportsCommand::class,
Commands\Reports\AutomatedReportsSendCommand::class,
Commands\MuteOrganizerChannel::class,
Commands\Tracks\DeleteTracks::class,
Commands\Tracks\RetryDownload::class,
Commands\Tracks\RetryFailedDownloads::class,
Commands\Twilio\SyncAddresses::class,
Commands\Activities\UpdateElasticSearch::class,
Commands\MakeSlackLiveCoachingChatNotesOn::class,
Commands\Activities\PreMeetingNotification::class,
ScheduleBotCommand::class,
ImportRegionMeetingCommand::class,
Commands\Activities\MonitorMeetingCountCommand::class,
Commands\Activities\MonitorMeetingStartCommand::class,
Commands\Activities\MonitorMeetingEndCommand::class,
Commands\SyncActivity::class,
Commands\PhpApm::class,
Commands\Crm\SyncOpportunity::class,
Commands\Crm\SyncLead::class,
Commands\Users\SyncLicenceDataToSalesforce::class,
Commands\Crm\UpdateOpportunitySpecifications::class,
Commands\Users\SyncToIntercom::class,
Commands\Users\SyncToUserPilot::class,
Commands\Teams\SyncToPlanhat::class,
Commands\Twilio\SetZoneAccess::class,
Commands\Users\CreateDefaultSavedSearchesCommand::class,
Commands\Crm\SendNotLogged::class,
Commands\Teams\DeactivateTeamCommand::class,
Commands\Crm\SyncFieldMetadata::class,
Commands\Postmark\SyncEmailTemplatesCommand::class,
Commands\PlaybackThemes\ImportTriggersFromTranslatedCsvCommand::class,
Commands\Activities\PreMeetingReminder::class,
Commands\Activities\CustomerActivitiesExport::class,
Commands\Users\RefreshAccessToken::class,
Commands\Calendars\SetupCalendarSubscription::class,
Commands\Activities\InviteMeetingBot::class,
Commands\Activities\ChangeActivitiesPlaybookCategoryOnPlaybookChange::class,
Commands\Crm\MigrateProvider::class,
Commands\Activities\MigrateLocationFromCalendarEventToActivities::class,
Commands\HelperTruncateCoachingTables::class,
Commands\FixCrossTenantIssues::class,
Commands\Activities\CloudCall\SetupIntegration::class,
Commands\Activities\CloudTalk\FixTimeZone::class,
Commands\Activities\Orum\SetupIntegration::class,
Commands\Activities\JustCall\SetupIntegration::class,
Commands\CoachingFeedbacksUpdateEsActivities::class,
Commands\Activities\RingCentral\AddInboundPromptSupport::class,
Commands\Dialers\Dialpad\SubscribeToWebhooks::class,
Commands\RecalculateDealRisksCommand::class,
SendDealsUpdateCommand::class,
Commands\Activities\SetProviderCapabilitiesField::class,
Commands\Teams\InitiallySetNotificationProviderTeamsTable::class,
Commands\Crm\AddLayoutEntities::class,
Commands\PropagateCoachingFeedbackCreatedAtToSectionFeedbacks::class,
Commands\JiminnyTokenInfoCommand::class,
Commands\JiminnySetEncryptedTokenManagerModeCommand::class,
Commands\EncryptTokensCommand::class,
Commands\UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll::class,
Commands\Dialers\Aircall\CheckAndRenewWebhooks::class,
Commands\Migrate\MigrateTeamRegionCommand::class,
Commands\ManageScimForTeam::class,
Commands\Dialers\SyncUsersCommand::class,
Commands\WhichWorkerIsWorkingOnWhichJob::class,
Commands\GroupSetDefaultLanguageCommand::class,
Commands\Dev\AddRateLimitCommand::class,
Commands\Dev\ImportCallsCommand::class,
Commands\DealInsights\BuildDealInsightsLayoutCommand::class,
Commands\DealInsights\DeleteAskJiminnyDealPrompts::class,
Commands\Crm\MatchCrmObjectsCommand::class,
Commands\Activities\SetupIntegration\EightByEight::class,
Commands\Calendars\RemoveCalendarEventActivitiesCommand::class,
Commands\Activities\Migrator\MigrateFromGongCommand::class,
Commands\Activities\Migrator\MigrateFromChorusCommand::class,
Commands\Activities\Migrator\MigrateFromLeexiCommand::class,
Commands\Activities\Migrator\MigrateFromAvomaCommand::class,
Commands\Activities\Migrator\MigrateFromClariCommand::class,
Commands\Activities\SetupIntegration\ConnectAndSell::class,
Commands\Activities\SetupIntegration\CloudTalk::class,
Commands\Users\CreateConferenceSlug::class,
Commands\Elasticsearch\AsyncUpdateEsActivities::class,
Commands\Elasticsearch\AsyncUpdateEsEntities::class,
Commands\Elasticsearch\ResetAsyncElasticSearchCommand::class,
Commands\Playlists\PlaylistSharesUpdateCommand::class,
Commands\Crm\AutologDelayedCommand::class,
Commands\Activities\HydrateDefaultActivityTypeCommand::class,
Commands\Crm\CheckActivityLoggableCommand::class,
Commands\Activities\MonitorDialerActivitiesCommand::class,
Commands\Activities\SetupIntegration\Xant::class,
Commands\ImportUsersFromCsvFile::class,
Commands\DevPostmanCommand::class,
Commands\Playlists\FixTreeStructureCommand::class,
Commands\Zoom\ResolvePmiLinksCommand::class,
Commands\MarkBranchForEnvironmentPipelineCommand::class,
Commands\Activities\ProbeMediaSegmentsCommand::class,
Commands\Activities\SetupIntegration\AmazonConnect::class,
Commands\Playbooks\ChangePlaybookActivityFieldCommand::class,
MediaPipelineRestartCommand::class,
Commands\Dev\FixHubSpotTokens::class,
Commands\Dev\MonitorSocialAccountsState::class,
Commands\Activities\SetupIntegration\Vonage::class,
Commands\Activities\SetupIntegration\TwilioFlex::class,
Commands\Activities\SetupIntegration\TwilioFlexDirect::class,
Commands\Activities\SetupIntegration\TwilioFlexSetDialerAuthCredentialsCommand::class,
SendActionItemsCommand::class,
Commands\Users\ChangeEmail::class,
Commands\Calendars\ListUserGoogleCalendars::class,
Commands\Activities\JustCall\SyncPlaybackLinkToCrmCommand::class,
Commands\Activities\HydrateCallWithCrmDataCommand::class,
Commands\Activities\UpdateActivityElasticSearchDocumentCommand::class,
Commands\Activities\SetupIntegration\Talkdesk::class,
Commands\Transcription\Microsoft\TranscriptionProviderMicrosoftWebhookRegisterCommand::class,
Commands\Transcription\Microsoft\TranscriptionProviderMicrosoftWebhookListCommand::class,
Commands\Transcription\Microsoft\TranscriptionProviderMicrosoftWebhookShow::class,
Commands\Transcription\Microsoft\TranscriptionProviderMicrosoftWebhookDeleteCommand::class,
Commands\Activities\SetupIntegration\TwilioVideo::class,
Commands\Crm\SetupCloseCrm::class,
Commands\Crm\SetupCopperCrm::class,
Commands\Crm\FullSyncOpportunityCommand::class,
Commands\Crm\IntegrationApp\CrmEntitiesFullSyncCommand::class,
Commands\Crm\IntegrationApp\ValidateConnectionCommand::class,
Commands\Activities\Workflow\RefreshCrmData::class,
Commands\Activities\Migrator\AnalyseGongCalls::class,
Commands\Users\AddVoiceRoleToRecorderCommand::class,
Commands\Activities\SyncMissingCallDispositions::class,
Commands\Calendars\RemoveFutureCalendarEvents::class,
FlushRolesPermissionsCache::class,
Commands\Activities\SetupIntegration\FiveNine::class,
CalendarEventDeleteCancelledCommand::class,
CalendarEventDeletePastCommand::class,
ReportActivityProcessingTimeToDatadogCommand::class,
ReportProcessingStatesToDatadogCommand::class,
ReleaseNumbersCommand::class,
RemoveExpiredRoleChangeEventsCommand::class,
RemoveExpiredNudgesCommand::class,
AutologOldActivitiesCommand::class,
RemoveUnusedParticipantSpeechesCommand::class,
DeleteActivitiesForChurnedTeamsCommand::class,
HardDeleteActivitiesForChurnedTeamsCommand::class,
TeamDeleteCommand::class,
TeamsDeleteDeactivatedCommand::class,
UpdateTeamsCommand::class,
OverrideTranscriptionLocaleCommand::class,
SyncSlackUserCommand::class,
PurgeSoftDeletedOpportunitiesCommand::class,
PurgeSyncBatchesCommand::class,
ProphetAnalyzeClosedDealsCommand::class,
DeleteChurnedSubAccounts::class,
Commands\ProphetAi\DumpContext::class,
DeletePredefinedSubAccounts::class,
DeleteActivitiesForRetentionTeamsCommand::class,
HardDeleteActivitiesTeamsCommand::class,
TeamsDeleteRetentionCommand::class,
TeamSettingPutCommand::class,
StopHangingLivestreamsCommand::class,
FixActivitiesOpportunity::class,
Commands\Activities\SetupIntegration\Salesforce\SetupSalesforceIntegrationCommand::class,
UpdateOldTranscriptionModelLocalesCommand::class,
Commands\Dev\FixMissMatchedCrmActivitiesCommand::class,
DownloadMissingTrackCommand::class,
ActivitiesMatchCrmCommand::class,
DeleteEmailDocumentsCommand::class,
DeleteOldTranscriptionsCommand::class,
DeleteS3LeftoversCommand::class,
RemoveDeleteMarkersCommand::class,
SyncTeamUsersCommand::class,
ReassignTranscriptCommand::class,
DiarizeViaAiParticipantIdentificationCommand::class,
RestoreActivityTypeCommand::class,
DeleteOldAiCrmNotesCommand::class,
DeleteReportCommand::class,
AutomatedReportsRetentionPolicyCommand::class,
SyncHubspotActiveDeals::class,
GenerateInternalWebhookToken::class,
RestoreActivityCrmProviderIdCommand::class,
DeleteUnusedTracksCommand::class,
RestoreTracksCommand::class,
HubspotWebhookServiceCommand::class,
ProcessMergedObjectsCommand::class,
HubspotJournalPollingCommand::class,
SetupJournalDealWebhookSubscriptionsCommand::class,
ListJournalWebhookSubscriptionsCommand::class,
RemoveGhostParticipantsCommand::class,
AutodetectAiActivityTypeCommand::class,
Commands\Crm\LogActivitiesCommand::class,
Commands\Crm\MatchOpportunityActivitiesCommand::class,
PurgeDeletedOpportunitiesCommand::class,
CleanDuplicateFieldDataCommand::class,
RetryProspectSummaryCommand::class,
ProcessHubspotObjectsSyncBatches::class,
SyncOpportunitiesMissingFieldDataCommand::class,
RestoreDealAssociationsCommand::class,
];
private Schedule $schedule;
private string $output;
protected function schedule(Schedule $schedule): void
{
$this->schedule = $schedule;
$this->output = config('jiminny.scheduler_log');
$schedule->useCache('redis');
$currentMinute = (int) date('i');
$currentDay = (int) date('w');
$this->scheduleEveryMinute();
$this->scheduleEveryTwoMinutes();
$this->scheduleEveryFiveMinutes();
$this->scheduleEveryTenMinutes();
$this->scheduleEveryFifteenMinutes();
$this->scheduleEveryThirtyMinutes();
$this->scheduleHourly();
$this->scheduleDaily();
$this->scheduleWeekly($currentDay);
$this->scheduleSpecificTimes();
$this->scheduleDynamic($currentMinute);
}
protected function scheduleEveryMinute(): void
{
$this->scheduleCommand('meeting-bot:schedule-bot', expiresAt: 1)->everyMinute();
$this->scheduleCommand('dialers:monitor-activities')->everyMinute();
$this->scheduleCommand('jiminny:monitor-social-accounts')->everyMinute();
$this->scheduleCommand('mailbox:skip-lists:refresh')->everyMinute();
$this->schedule->command('mailbox:batch:process', ['--max-batches=15'])
->everyMinute()
->sendOutputTo($this->output);
}
protected function scheduleEveryTwoMinutes(): void
{
$this->scheduleCommand('conference:monitor:count', [], 2)->everyTwoMinutes();
}
protected function scheduleEveryFiveMinutes(): void
{
$this->scheduleCommand('activity:purge-stale', [], 4)->everyFiveMinutes();
// Offset by 1 minute to avoid overlap with crm:sync-objects (runs at :14 and :44)
$this->scheduleCommand('crm:sync-hubspot-objects', [], 4)
->cron('1,6,11,16,21,26,31,36,41,46,51,56 * * * *');
$this->scheduleCommand('mailbox:text-relay:sync')->everyFiveMinutes();
$this->scheduleCommand('conference:pre-meeting-notification', [], 3)->everyFiveMinutes();
$this->scheduleCommand('conference:monitor:start', expiresAt: 3)->everyFiveMinutes();
$this->scheduleCommand('conference:monitor:end', expiresAt: 3)->everyFiveMinutes();
$this->scheduleCommand('jiminny:fix-hubspot-tokens')->everyFiveMinutes();
$this->scheduleCommand('conference:pre-meeting-reminder')->everyFiveMinutes()->runInBackground();
$this->schedule->command('mailbox:batch:create')
->cron('2-59/5 * * * *')
->withoutOverlapping(180)
->onOneServer()
->sendOutputTo($this->output);
$this->schedule->command('mailbox:batch:retry-failed', ['--max-batches=15'])
->cron('3-59/5 * * * *')
->withoutOverlapping(180)
->onOneServer()
->sendOutputTo($this->output)
->runInBackground();
$this->schedule->command('hubspot:journal-poll', ['--start'])
->everyFiveMinutes()
->sendOutputTo($this->output)
->runInBackground();
}
protected function scheduleEveryTenMinutes(): void
{
$this->scheduleCommand('jiminny:transcription:retry-failed')->everyTenMinutes();
$this->scheduleCommand('activity:notify-not-logged')->cron('6,16,26,36,46,56 * * * *');
$this->scheduleCommand('activity:status-count')->cron('6,16,26,36,46,56 * * * *');
$this->scheduleCommand('mailbox:sync')->cron('6,16,26,36,46,56 * * * *');
$this->scheduleCommand('crm:reset-governor')->everyTenMinutes();
}
protected function scheduleEveryFifteenMinutes(): void
{
$this->scheduleCommand('datadog:report:processing-sla-activities')->everyFifteenMinutes();
$this->scheduleCommand('calendar:sync', ['--dateMode=daily'], 14)->cron('13,28,43,58 * * * *');
$this->scheduleCommand('activity:aircall:check-and-renew')->cron('9,24,39,54 * * * *');
$this->scheduleCommand('track:retry-failed-downloads')->cron('9,24,39,54 * * * *');
$this->scheduleCommand('crm:autolog-delayed')->cron('3,18,33,48 * * * *');
$this->scheduleCommand('activity:sync', [
'--from' => now()->subMinutes(16)->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--skipProviders' => [
Activity::PROVIDER_RINGCENTRAL,
Activity::PROVIDER_AVAYA,
Activity::PROVIDER_TELUS,
Activity::PROVIDER_TALKDESK,
],
])->everyFifteenMinutes();
$this->scheduleCommand('activity:sync', [
Activity::PROVIDER_RINGCENTRAL,
Activity::PROVIDER_AVAYA,
Activity::PROVIDER_TELUS,
Activity::PROVIDER_TALKDESK,
'--from' => now()->subMinutes(16)->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
])->cron('7,22,37,52 * * * *');
}
protected function scheduleEveryThirtyMinutes(): void
{
$this->scheduleCommand('crm:sync-objects')->cron('14,44 * * * *');
$this->scheduleCommand('mailbox:batch:fail-stalled')->everyThirtyMinutes();
$this->scheduleCommand('activities:delete-activities-for-deactivated-teams', expiresAt: 5)
->between('02:58', '05:29')
->everyThirtyMinutes()
->runInBackground();
$this->scheduleActivitiesHardDelete();
}
protected function scheduleHourly(): void
{
$this->scheduleCommand('jiminny:transcription:retry-stuck')->hourly();
$this->scheduleCommand('twilio:recover-tracks')->cron('22 * * * *');
$this->scheduleCommand('dialers:sync-users')->cron('22 * * * *');
$this->scheduleCommand('datadog:report:failed-processing-states')->cron('22 * * * *');
$this->scheduleCommand('automated-reports:send')->hourly();
$this->scheduleCommand('deal-insights:send-update')->hourlyAt(0);
$this->scheduleCommand('crm:integration-app-validate-team-connection')->hourlyAt(23);
}
protected function scheduleDaily(): void
{
$this->scheduleCommand('teams:sync-planhat')->daily();
$this->scheduleCommand('twilio:sync-addresses')->daily();
$this->scheduleCommand('twilio:sync-zone-access')->daily();
$this->scheduleCommand('mailbox:text-relay:watch-text-events')->daily();
$this->scheduleCommand('users:sync-licence-data')->daily();
$this->scheduleCommand('users:sync-intercom-data')->daily();
}
protected function scheduleWeekly(int $currentDay): void
{
if ($currentDay === 0) {
$this->scheduleCommand('crm:update-opp-specs')->weeklyOn(0);
}
if ($currentDay === 6) {
$this->scheduleCommand('jiminny:acl:remove-expired-role-change-events')->saturdays();
$this->scheduleCommand('activity:sync', [
Activity::PROVIDER_AMAZON_CONNECT,
'--from' => now()->subDays(7)->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
])->saturdays()->at('01:00')->runInBackground();
$this->scheduleCommand('calendar:event:delete-past', ['--force'], 60)
->saturdays()->at('01:07')->runInBackground();
$this->scheduleCommand('calendar:event:delete-cancelled', ['--force'], 60 * 47 + 52)
->saturdays()->at('05:08')->runInBackground();
$this->scheduleCommand('nudges-data-clean-up --squashNudgeRuns')
->weeklyOn(6, '6:00');
}
}
protected function scheduleSpecificTimes(): void
{
$this->scheduleCommand('deal-risks:calculate', ['--cronjob'])->dailyAt('00:00');
$this->scheduleCommand(DeleteInboxEmailsCommand::NAME, [
'--status' => InboxEmail::STATUS_DISCARDED,
'--to' => now()->subWeeks(2)->format('Y-m-d'),
])->saturdays()->at('00:20')->runInBackground();
$this->scheduleCommand(DeleteInboxEmailsCommand::NAME, [
'--status' => InboxEmail::STATUS_PROCESSED,
'--to' => now()->subWeeks(2)->format('Y-m-d'),
])->saturdays()->at('00:30')->runInBackground();
$this->scheduleCommand('automated-reports')->dailyAt('01:00');
$this->scheduleCommand('crm:sync-team-metadata')->dailyAt('01:05');
$this->scheduleCommand('crm:sync-profile-metadata')->dailyAt('01:05');
$this->scheduleCommand('calendar:sync-deleted-events')->dailyAt('01:10');
$this->scheduleCommand('teams:delete-retention')->dailyAt('02:55');
$this->scheduleCommand('teams:delete-deactivated')->dailyAt('02:58');
$this->scheduleCommand('twilio:remote-lifecycle')->dailyAt('03:00');
$this->scheduleCommand('activity:sync', [
Activity::PROVIDER_VONAGE,
Activity::PROVIDER_FIVE_NINE,
'--from' => now()->subDay()->startOfDay()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->subDay()->endOfDay()->format(SyncActivity::ALLOWED_DATE_FORMAT),
])->dailyAt('03:05');
$this->scheduleCommand('activities:delete-retention-teams', expiresAt: 240)->dailyAt('03:04');
$this->scheduleCommand('automated-reports:run-retention-policy', expiresAt: 120)->dailyAt('03:15');
$this->scheduleCommand('stop:hanging:livestreams')->dailyAt('03:30');
$this->scheduleCommand('crm:purge-sync-batches')->dailyAt('03:45');
$this->scheduleCommand('twilio:sync-numbers')->dailyAt('04:00');
if (! $this->app->environment('production')) {
$this->scheduleCommand('activities:hard-delete', ['--limit' => 1000, '--jobs' => 5], 60)
->dailyAt('04:02')->runInBackground();
}
$this->scheduleCommand('crm:full-sync-opportunity')->dailyAt('05:00');
$this->scheduleCommand('activity:sync', [
'--from' => now()->subDay()->startOfDay()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->subDay()->endOfDay()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--skipProviders' => [
Activity::PROVIDER_VONAGE,
Activity::PROVIDER_FIVE_NINE,
],
])->dailyAt('05:05');
if (! $this->app->environment('qa')) {
$this->scheduleCommand('ai-crm-notes:delete-old')->dailyAt('07:00');
}
$this->scheduleCommand('activity:sync-dispositions', [
Activity::PROVIDER_HUBSPOT,
'--from' => now()->subDay()->startOfDay()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->subDay()->endOfDay()->format(SyncActivity::ALLOWED_DATE_FORMAT),
])->dailyAt('07:05');
}
protected function scheduleDynamic(int $currentMinute): void
{
$this->scheduleHourlyFallbackActivitySyncs($currentMinute);
$this->scheduleBullhornHeartbeat($currentMinute);
}
private function scheduleHourlyFallbackActivitySyncs(int $offsetMinute): void
{
if ($offsetMinute === 0) {
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_CLOUD_TALK, 3, 0);
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_VONAGE, 6, 0);
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_CLOUDCALL, 3, 0);
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_CLOUDCALL_US, 3, 0);
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_FIVE_NINE, 3, 0);
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_HUBSPOT, 1, 0);
} elseif ($offsetMinute === 1) {
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_RINGCENTRAL, Constants::RINGCENTRAL_CALL_LOG_LOOK_BACK_HOURS, 1);
} elseif ($offsetMinute === 2) {
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_AVAYA, Constants::RINGCENTRAL_CALL_LOG_LOOK_BACK_HOURS, 2);
}
}
private function scheduleBullhornHeartbeat(int $currentMinute): void
{
$bhHeartbeatInterval = config('services.bullhorn.heartbeatInterval', 0);
if ($bhHeartbeatInterval > 0) {
$minutes = max((int) floor($bhHeartbeatInterval / 60), 1);
if ($currentMinute % $minutes === 0) {
$bhEvent = $this->scheduleCommand('crm:bullhorn:ping', ['--heartbeat']);
if ($minutes > 30) {
$bhEvent->hourly();
} else {
$bhEvent->cron(sprintf('*/%d * * * *', $minutes));
}
}
}
}
private function scheduleActivitiesHardDelete(): void
{
if (config(key: 'jiminny.deploy_region') === 'eu') {
$this->scheduleCommand(
name: 'activities:hard-delete',
options: ['--limit' => 1000, '--jobs' => 20],
expiresAt: 29
)
->between('02:59', '07:02')->everyThirtyMinutes()
->runInBackground();
} elseif ($this->app->environment('production')) {
$this->scheduleCommand(
name: 'activities:hard-delete',
options: ['--limit' => 2000, '--jobs' => 20],
expiresAt: 29
)
->between('02:59', '07:02')->everyThirtyMinutes()
->runInBackground();
}
}
private function scheduleHourlyFallbackActivitySync(string $provider, int $hours, int $offsetMinute = 0): void
{
$this->scheduleCommand('activity:sync', [
$provider,
'--from' => now()->subHours($hours)->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
])->hourlyAt($offsetMinute);
}
/**
* Register the Closure based commands for the application.
*/
protected function commands(): void
{
require_once base_path('routes/console.php');
}
private function scheduleCommand(string $name, array $options = [], $expiresAt = 60 * 3): Event
{
return $this->schedule
->command($name, $options)
->withoutOverlapping($expiresAt)
->onOneServer()
->sendOutputTo($this->output)
;
}
}
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...
|
PhpStorm
|
faVsco.js – Kernel.php
|
NULL
|
76714
|
|
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
Analyzing…
<?php
namespace Jiminny\Console;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Jiminny\Component\Acl\RemoveExpiredRoleChangeEventsCommand;
use Jiminny\Component\ActionItems\Commands\SendActionItemsCommand;
use Jiminny\Component\AiActivityType\Commands\AutodetectAiActivityTypeCommand;
use Jiminny\Component\AskJiminnyAi\Commands\ProphetAnalyzeClosedDealsCommand;
use Jiminny\Component\Cache\Constants;
use Jiminny\Component\DealInsights\Commands\SendDealsUpdateCommand;
use Jiminny\Component\MediaPipeline\Command\MediaPipelineRestartCommand;
use Jiminny\Component\MediaPipeline\Command\ReportActivityProcessingTimeToDatadogCommand;
use Jiminny\Component\MediaPipeline\Command\ReportProcessingStatesToDatadogCommand;
use Jiminny\Component\Transcription\Commands\OverrideTranscriptionLocaleCommand;
use Jiminny\Component\Transcription\Commands\RetryFailedTranscriptionsCommand;
use Jiminny\Component\Transcription\Commands\RetryStuckTranscriptionsCommand;
use Jiminny\Console\Commands\Activities\ActivitiesMatchCrmCommand;
use Jiminny\Console\Commands\Activities\AutologOldActivitiesCommand;
use Jiminny\Console\Commands\Activities\DeleteActivitiesForChurnedTeamsCommand;
use Jiminny\Console\Commands\Activities\DeleteActivitiesForRetentionTeamsCommand;
use Jiminny\Console\Commands\Activities\DownloadMissingTrackCommand;
use Jiminny\Console\Commands\Activities\FixActivitiesOpportunity;
use Jiminny\Console\Commands\Activities\HardDeleteActivitiesForChurnedTeamsCommand;
use Jiminny\Console\Commands\Activities\HardDeleteActivitiesTeamsCommand;
use Jiminny\Console\Commands\Activities\ReassignTranscriptCommand;
use Jiminny\Console\Commands\Activities\ReindexRecentActivitiesCommand;
use Jiminny\Console\Commands\Activities\RetryProspectSummaryCommand;
use Jiminny\Console\Commands\Calendars\Events\CalendarEventDeleteCancelledCommand;
use Jiminny\Console\Commands\Calendars\Events\CalendarEventDeletePastCommand;
use Jiminny\Console\Commands\Crm\CleanDuplicateFieldDataCommand;
use Jiminny\Console\Commands\Crm\Hubspot\ProcessMergedObjectsCommand;
use Jiminny\Console\Commands\Crm\Hubspot\RestoreDealAssociationsCommand;
use Jiminny\Console\Commands\Crm\ProcessHubspotObjectsSyncBatches;
use Jiminny\Console\Commands\Crm\PurgeDeletedOpportunitiesCommand;
use Jiminny\Console\Commands\Crm\Hubspot\ListJournalWebhookSubscriptionsCommand;
use Jiminny\Console\Commands\Crm\Hubspot\SetupJournalDealWebhookSubscriptionsCommand;
use Jiminny\Console\Commands\Crm\SyncHubspotActiveDeals;
use Jiminny\Console\Commands\Crm\SyncOpportunitiesMissingFieldDataCommand;
use Jiminny\Console\Commands\DeleteOldAiCrmNotesCommand;
use Jiminny\Console\Commands\DeleteS3LeftoversCommand;
use Jiminny\Console\Commands\DiarizeViaAiParticipantIdentificationCommand;
use Jiminny\Console\Commands\Elasticsearch\DeleteEmailDocumentsCommand;
use Jiminny\Console\Commands\Elasticsearch\RemoveGhostParticipantsCommand;
use Jiminny\Console\Commands\FlushRolesPermissionsCache;
use Jiminny\Console\Commands\GenerateInternalWebhookToken;
use Jiminny\Console\Commands\HubspotJournalPollingCommand;
use Jiminny\Console\Commands\HubspotWebhookServiceCommand;
use Jiminny\Console\Commands\Livestream\StopHangingLivestreamsCommand;
use Jiminny\Console\Commands\Mailboxes\DeleteEmailMessagesWithoutActivityCommand;
use Jiminny\Console\Commands\Mailboxes\DeleteInboxEmailsCommand;
use Jiminny\Console\Commands\PurgeSoftDeletedOpportunitiesCommand;
use Jiminny\Console\Commands\PurgeSyncBatchesCommand;
use Jiminny\Console\Commands\RemoveDeleteMarkersCommand;
use Jiminny\Console\Commands\RemoveExpiredNudgesCommand;
use Jiminny\Console\Commands\RemoveUnusedParticipantSpeechesCommand;
use Jiminny\Console\Commands\Reports\AutomatedReportsRetentionPolicyCommand;
use Jiminny\Console\Commands\Reports\DeleteReportCommand;
use Jiminny\Console\Commands\RestoreActivityCrmProviderIdCommand;
use Jiminny\Console\Commands\RestoreActivityTypeCommand;
use Jiminny\Console\Commands\Slack\SyncSlackUserCommand;
use Jiminny\Console\Commands\Teams\SyncTeamUsersCommand;
use Jiminny\Console\Commands\Teams\TeamDeleteCommand;
use Jiminny\Console\Commands\Teams\TeamsDeleteDeactivatedCommand;
use Jiminny\Console\Commands\Teams\TeamsDeleteRetentionCommand;
use Jiminny\Console\Commands\Teams\TeamSettingPutCommand;
use Jiminny\Console\Commands\Teams\UpdateTeamsCommand;
use Jiminny\Console\Commands\Tracks\DeleteUnusedTracksCommand;
use Jiminny\Console\Commands\Tracks\RestoreTracksCommand;
use Jiminny\Console\Commands\Transcription\DeleteOldTranscriptionsCommand;
use Jiminny\Console\Commands\Transcription\UpdateOldTranscriptionModelLocalesCommand;
use Jiminny\Console\Commands\Twilio\DeleteChurnedSubAccounts;
use Jiminny\Console\Commands\Twilio\DeletePredefinedSubAccounts;
use Jiminny\Console\Commands\Twilio\ReleaseNumbersCommand;
use Jiminny\Jobs\Activity\SyncActivity;
use Jiminny\Models\Activity;
use Jiminny\Models\InboxEmail;
use Jiminny\Services\RecallAI\Commands\ImportRegionMeetingCommand;
use Jiminny\Services\RecallAI\Commands\ScheduleBotCommand;
class Kernel extends ConsoleKernel
{
use ConfirmableTrait;
/**
* The Artisan commands provided by your application.
*
* @var string[]
*/
protected $commands = [
Commands\GeckoExport\GeckoExportTranscriptCommand::class,
Commands\GeckoExport\GeckoExportTranscriptionCommand::class,
Commands\GeckoExport\GeckoExportParticipantSpeechesCommand::class,
Commands\Activities\DeleteForCoachesCommand::class,
ReindexRecentActivitiesCommand::class,
Commands\Crm\BullhornPingCommand::class,
Commands\Crm\BullhornSessionCommand::class,
Commands\Crm\BullhornSearchCommand::class,
Commands\PlaybackThemes\TopicsConsolidateCommand::class,
Commands\PlaybackThemes\PlaybackThemesCopyCommand::class,
Commands\PlaybackThemes\AssignTopicsUsedBySingleTeamCommand::class,
Commands\PlaybackThemes\PlaybackThemesMigrateToVersionsCommand::class,
Commands\Vocabulary\VocabularyCopyCommand::class,
Commands\Transcription\TranscriptionPrintRaw::class,
Commands\Migrate\JiminnyMigratePopulateActivitySourceCommand::class,
Commands\EngagementStats\JiminnyEngagementStatsExplainCommand::class,
Commands\EngagementStatsRegenerateCommand::class,
Commands\Analytics\NumberOfActivitiesPerActivityTypeCommand::class,
Commands\Elasticsearch\MappingRunCommand::class,
Commands\Elasticsearch\MappingInstallCommand::class,
Commands\Elasticsearch\UpdateEsMappingSettingsCommand::class,
Commands\Analytics\TranscriptionWordMatchCommand::class,
Commands\JiminnyCacheClearCommand::class,
Commands\Transcription\TranscriptionSearchCommand::class,
RetryStuckTranscriptionsCommand::class,
RetryFailedTranscriptionsCommand::class,
Commands\JiminnyDebugCommand::class,
Commands\Calendars\SyncCalendars::class,
Commands\Calendars\SyncDeletedEvents::class,
Commands\Twilio\FetchMetrics::class,
Commands\Twilio\FetchEvents::class,
Commands\Twilio\FetchSummary::class,
Commands\Twilio\SyncZoneAccess::class,
Commands\DatabaseTableCount::class,
Commands\PurgeConferences::class,
Commands\ResetElasticSearch::class,
Commands\CreateDatabaseUsers::class,
Commands\Activities\NotifyNotLogged::class,
Commands\Crm\SyncTeamMetadata::class,
Commands\Crm\SyncProfileMetadata::class,
Commands\Crm\SyncContact::class,
Commands\Crm\SyncObjects::class,
Commands\Crm\SyncHubspotObjects::class,
Commands\Crm\SyncAccount::class,
Commands\Crm\ResetGovernorLimits::class,
Commands\Crm\ManageSyncStrategyCommand::class,
Commands\ImportRecording::class,
Commands\TrackImported::class,
Commands\Twilio\RecoverTwilioTracksCommand::class,
Commands\Crm\SetupLayouts::class,
Commands\Tracks\SyncTwilioTracks::class,
Commands\Activities\StatusCount::class,
Commands\Mailboxes\TextRelay\WatchMailboxEvents::class,
Commands\Mailboxes\InboxCreate::class,
Commands\Mailboxes\InboxSync::class,
Commands\Mailboxes\BatchCreate::class,
Commands\Mailboxes\BatchProcess::class,
Commands\Mailboxes\InboxPurge::class,
Commands\Mailboxes\BatchRetryFailed::class,
Commands\Mailboxes\BatchFailStalled::class,
Commands\Mailboxes\SkipListsRefresh::class,
Commands\Mailboxes\SkipListsDump::class,
Commands\Mailboxes\TextRelay\SyncMailbox::class,
Commands\Mailboxes\DeleteInboxEmailsCommand::class,
Commands\Mailboxes\DeleteEmailMessagesCommand::class,
DeleteEmailMessagesWithoutActivityCommand::class,
Commands\Tracks\CheckIntegrity::class,
Commands\Twilio\RemoteLifecycle::class,
Commands\Twilio\SyncNumbers::class,
Commands\Crm\SetupActivityTypeForFollowUp::class,
Commands\Activities\CheckPlayable::class,
Commands\Activities\ActivityDeleteCommand::class,
Commands\Activities\Copy::class,
Commands\Activities\ActivityHardDeleteCommand::class,
Commands\Reports\Team::class,
Commands\Reports\GenerateMarketingReport::class,
Commands\Reports\AutomatedReportsCommand::class,
Commands\Reports\AutomatedReportsSendCommand::class,
Commands\MuteOrganizerChannel::class,
Commands\Tracks\DeleteTracks::class,
Commands\Tracks\RetryDownload::class,
Commands\Tracks\RetryFailedDownloads::class,
Commands\Twilio\SyncAddresses::class,
Commands\Activities\UpdateElasticSearch::class,
Commands\MakeSlackLiveCoachingChatNotesOn::class,
Commands\Activities\PreMeetingNotification::class,
ScheduleBotCommand::class,
ImportRegionMeetingCommand::class,
Commands\Activities\MonitorMeetingCountCommand::class,
Commands\Activities\MonitorMeetingStartCommand::class,
Commands\Activities\MonitorMeetingEndCommand::class,
Commands\SyncActivity::class,
Commands\PhpApm::class,
Commands\Crm\SyncOpportunity::class,
Commands\Crm\SyncLead::class,
Commands\Users\SyncLicenceDataToSalesforce::class,
Commands\Crm\UpdateOpportunitySpecifications::class,
Commands\Users\SyncToIntercom::class,
Commands\Users\SyncToUserPilot::class,
Commands\Teams\SyncToPlanhat::class,
Commands\Twilio\SetZoneAccess::class,
Commands\Users\CreateDefaultSavedSearchesCommand::class,
Commands\Crm\SendNotLogged::class,
Commands\Teams\DeactivateTeamCommand::class,
Commands\Crm\SyncFieldMetadata::class,
Commands\Postmark\SyncEmailTemplatesCommand::class,
Commands\PlaybackThemes\ImportTriggersFromTranslatedCsvCommand::class,
Commands\Activities\PreMeetingReminder::class,
Commands\Activities\CustomerActivitiesExport::class,
Commands\Users\RefreshAccessToken::class,
Commands\Calendars\SetupCalendarSubscription::class,
Commands\Activities\InviteMeetingBot::class,
Commands\Activities\ChangeActivitiesPlaybookCategoryOnPlaybookChange::class,
Commands\Crm\MigrateProvider::class,
Commands\Activities\MigrateLocationFromCalendarEventToActivities::class,
Commands\HelperTruncateCoachingTables::class,
Commands\FixCrossTenantIssues::class,
Commands\Activities\CloudCall\SetupIntegration::class,
Commands\Activities\CloudTalk\FixTimeZone::class,
Commands\Activities\Orum\SetupIntegration::class,
Commands\Activities\JustCall\SetupIntegration::class,
Commands\CoachingFeedbacksUpdateEsActivities::class,
Commands\Activities\RingCentral\AddInboundPromptSupport::class,
Commands\Dialers\Dialpad\SubscribeToWebhooks::class,
Commands\RecalculateDealRisksCommand::class,
SendDealsUpdateCommand::class,
Commands\Activities\SetProviderCapabilitiesField::class,
Commands\Teams\InitiallySetNotificationProviderTeamsTable::class,
Commands\Crm\AddLayoutEntities::class,
Commands\PropagateCoachingFeedbackCreatedAtToSectionFeedbacks::class,
Commands\JiminnyTokenInfoCommand::class,
Commands\JiminnySetEncryptedTokenManagerModeCommand::class,
Commands\EncryptTokensCommand::class,
Commands\UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll::class,
Commands\Dialers\Aircall\CheckAndRenewWebhooks::class,
Commands\Migrate\MigrateTeamRegionCommand::class,
Commands\ManageScimForTeam::class,
Commands\Dialers\SyncUsersCommand::class,
Commands\WhichWorkerIsWorkingOnWhichJob::class,
Commands\GroupSetDefaultLanguageCommand::class,
Commands\Dev\AddRateLimitCommand::class,
Commands\Dev\ImportCallsCommand::class,
Commands\DealInsights\BuildDealInsightsLayoutCommand::class,
Commands\DealInsights\DeleteAskJiminnyDealPrompts::class,
Commands\Crm\MatchCrmObjectsCommand::class,
Commands\Activities\SetupIntegration\EightByEight::class,
Commands\Calendars\RemoveCalendarEventActivitiesCommand::class,
Commands\Activities\Migrator\MigrateFromGongCommand::class,
Commands\Activities\Migrator\MigrateFromChorusCommand::class,
Commands\Activities\Migrator\MigrateFromLeexiCommand::class,
Commands\Activities\Migrator\MigrateFromAvomaCommand::class,
Commands\Activities\Migrator\MigrateFromClariCommand::class,
Commands\Activities\SetupIntegration\ConnectAndSell::class,
Commands\Activities\SetupIntegration\CloudTalk::class,
Commands\Users\CreateConferenceSlug::class,
Commands\Elasticsearch\AsyncUpdateEsActivities::class,
Commands\Elasticsearch\AsyncUpdateEsEntities::class,
Commands\Elasticsearch\ResetAsyncElasticSearchCommand::class,
Commands\Playlists\PlaylistSharesUpdateCommand::class,
Commands\Crm\AutologDelayedCommand::class,
Commands\Activities\HydrateDefaultActivityTypeCommand::class,
Commands\Crm\CheckActivityLoggableCommand::class,
Commands\Activities\MonitorDialerActivitiesCommand::class,
Commands\Activities\SetupIntegration\Xant::class,
Commands\ImportUsersFromCsvFile::class,
Commands\DevPostmanCommand::class,
Commands\Playlists\FixTreeStructureCommand::class,
Commands\Zoom\ResolvePmiLinksCommand::class,
Commands\MarkBranchForEnvironmentPipelineCommand::class,
Commands\Activities\ProbeMediaSegmentsCommand::class,
Commands\Activities\SetupIntegration\AmazonConnect::class,
Commands\Playbooks\ChangePlaybookActivityFieldCommand::class,
MediaPipelineRestartCommand::class,
Commands\Dev\FixHubSpotTokens::class,
Commands\Dev\MonitorSocialAccountsState::class,
Commands\Activities\SetupIntegration\Vonage::class,
Commands\Activities\SetupIntegration\TwilioFlex::class,
Commands\Activities\SetupIntegration\TwilioFlexDirect::class,
Commands\Activities\SetupIntegration\TwilioFlexSetDialerAuthCredentialsCommand::class,
SendActionItemsCommand::class,
Commands\Users\ChangeEmail::class,
Commands\Calendars\ListUserGoogleCalendars::class,
Commands\Activities\JustCall\SyncPlaybackLinkToCrmCommand::class,
Commands\Activities\HydrateCallWithCrmDataCommand::class,
Commands\Activities\UpdateActivityElasticSearchDocumentCommand::class,
Commands\Activities\SetupIntegration\Talkdesk::class,
Commands\Transcription\Microsoft\TranscriptionProviderMicrosoftWebhookRegisterCommand::class,
Commands\Transcription\Microsoft\TranscriptionProviderMicrosoftWebhookListCommand::class,
Commands\Transcription\Microsoft\TranscriptionProviderMicrosoftWebhookShow::class,
Commands\Transcription\Microsoft\TranscriptionProviderMicrosoftWebhookDeleteCommand::class,
Commands\Activities\SetupIntegration\TwilioVideo::class,
Commands\Crm\SetupCloseCrm::class,
Commands\Crm\SetupCopperCrm::class,
Commands\Crm\FullSyncOpportunityCommand::class,
Commands\Crm\IntegrationApp\CrmEntitiesFullSyncCommand::class,
Commands\Crm\IntegrationApp\ValidateConnectionCommand::class,
Commands\Activities\Workflow\RefreshCrmData::class,
Commands\Activities\Migrator\AnalyseGongCalls::class,
Commands\Users\AddVoiceRoleToRecorderCommand::class,
Commands\Activities\SyncMissingCallDispositions::class,
Commands\Calendars\RemoveFutureCalendarEvents::class,
FlushRolesPermissionsCache::class,
Commands\Activities\SetupIntegration\FiveNine::class,
CalendarEventDeleteCancelledCommand::class,
CalendarEventDeletePastCommand::class,
ReportActivityProcessingTimeToDatadogCommand::class,
ReportProcessingStatesToDatadogCommand::class,
ReleaseNumbersCommand::class,
RemoveExpiredRoleChangeEventsCommand::class,
RemoveExpiredNudgesCommand::class,
AutologOldActivitiesCommand::class,
RemoveUnusedParticipantSpeechesCommand::class,
DeleteActivitiesForChurnedTeamsCommand::class,
HardDeleteActivitiesForChurnedTeamsCommand::class,
TeamDeleteCommand::class,
TeamsDeleteDeactivatedCommand::class,
UpdateTeamsCommand::class,
OverrideTranscriptionLocaleCommand::class,
SyncSlackUserCommand::class,
PurgeSoftDeletedOpportunitiesCommand::class,
PurgeSyncBatchesCommand::class,
ProphetAnalyzeClosedDealsCommand::class,
DeleteChurnedSubAccounts::class,
Commands\ProphetAi\DumpContext::class,
DeletePredefinedSubAccounts::class,
DeleteActivitiesForRetentionTeamsCommand::class,
HardDeleteActivitiesTeamsCommand::class,
TeamsDeleteRetentionCommand::class,
TeamSettingPutCommand::class,
StopHangingLivestreamsCommand::class,
FixActivitiesOpportunity::class,
Commands\Activities\SetupIntegration\Salesforce\SetupSalesforceIntegrationCommand::class,
UpdateOldTranscriptionModelLocalesCommand::class,
Commands\Dev\FixMissMatchedCrmActivitiesCommand::class,
DownloadMissingTrackCommand::class,
ActivitiesMatchCrmCommand::class,
DeleteEmailDocumentsCommand::class,
DeleteOldTranscriptionsCommand::class,
DeleteS3LeftoversCommand::class,
RemoveDeleteMarkersCommand::class,
SyncTeamUsersCommand::class,
ReassignTranscriptCommand::class,
DiarizeViaAiParticipantIdentificationCommand::class,
RestoreActivityTypeCommand::class,
DeleteOldAiCrmNotesCommand::class,
DeleteReportCommand::class,
AutomatedReportsRetentionPolicyCommand::class,
SyncHubspotActiveDeals::class,
GenerateInternalWebhookToken::class,
RestoreActivityCrmProviderIdCommand::class,
DeleteUnusedTracksCommand::class,
RestoreTracksCommand::class,
HubspotWebhookServiceCommand::class,
ProcessMergedObjectsCommand::class,
HubspotJournalPollingCommand::class,
SetupJournalDealWebhookSubscriptionsCommand::class,
ListJournalWebhookSubscriptionsCommand::class,
RemoveGhostParticipantsCommand::class,
AutodetectAiActivityTypeCommand::class,
Commands\Crm\LogActivitiesCommand::class,
Commands\Crm\MatchOpportunityActivitiesCommand::class,
PurgeDeletedOpportunitiesCommand::class,
CleanDuplicateFieldDataCommand::class,
RetryProspectSummaryCommand::class,
ProcessHubspotObjectsSyncBatches::class,
SyncOpportunitiesMissingFieldDataCommand::class,
RestoreDealAssociationsCommand::class,
];
private Schedule $schedule;
private string $output;
protected function schedule(Schedule $schedule): void
{
$this->schedule = $schedule;
$this->output = config('jiminny.scheduler_log');
$schedule->useCache('redis');
$currentMinute = (int) date('i');
$currentDay = (int) date('w');
$this->scheduleEveryMinute();
$this->scheduleEveryTwoMinutes();
$this->scheduleEveryFiveMinutes();
$this->scheduleEveryTenMinutes();
$this->scheduleEveryFifteenMinutes();
$this->scheduleEveryThirtyMinutes();
$this->scheduleHourly();
$this->scheduleDaily();
$this->scheduleWeekly($currentDay);
$this->scheduleSpecificTimes();
$this->scheduleDynamic($currentMinute);
}
protected function scheduleEveryMinute(): void
{
$this->scheduleCommand('meeting-bot:schedule-bot', expiresAt: 1)->everyMinute();
$this->scheduleCommand('dialers:monitor-activities')->everyMinute();
$this->scheduleCommand('jiminny:monitor-social-accounts')->everyMinute();
$this->scheduleCommand('mailbox:skip-lists:refresh')->everyMinute();
$this->schedule->command('mailbox:batch:process', ['--max-batches=15'])
->everyMinute()
->sendOutputTo($this->output);
}
protected function scheduleEveryTwoMinutes(): void
{
$this->scheduleCommand('conference:monitor:count', [], 2)->everyTwoMinutes();
}
protected function scheduleEveryFiveMinutes(): void
{
$this->scheduleCommand('activity:purge-stale', [], 4)->everyFiveMinutes();
// Offset by 1 minute to avoid overlap with crm:sync-objects (runs at :14 and :44)
$this->scheduleCommand('crm:sync-hubspot-objects', [], 4)
->cron('1,6,11,16,21,26,31,36,41,46,51,56 * * * *');
$this->scheduleCommand('mailbox:text-relay:sync')->everyFiveMinutes();
$this->scheduleCommand('conference:pre-meeting-notification', [], 3)->everyFiveMinutes();
$this->scheduleCommand('conference:monitor:start', expiresAt: 3)->everyFiveMinutes();
$this->scheduleCommand('conference:monitor:end', expiresAt: 3)->everyFiveMinutes();
$this->scheduleCommand('jiminny:fix-hubspot-tokens')->everyFiveMinutes();
$this->scheduleCommand('conference:pre-meeting-reminder')->everyFiveMinutes()->runInBackground();
$this->schedule->command('mailbox:batch:create')
->cron('2-59/5 * * * *')
->withoutOverlapping(180)
->onOneServer()
->sendOutputTo($this->output);
$this->schedule->command('mailbox:batch:retry-failed', ['--max-batches=15'])
->cron('3-59/5 * * * *')
->withoutOverlapping(180)
->onOneServer()
->sendOutputTo($this->output)
->runInBackground();
$this->schedule->command('hubspot:journal-poll', ['--start'])
->everyFiveMinutes()
->sendOutputTo($this->output)
->runInBackground();
}
protected function scheduleEveryTenMinutes(): void
{
$this->scheduleCommand('jiminny:transcription:retry-failed')->everyTenMinutes();
$this->scheduleCommand('activity:notify-not-logged')->cron('6,16,26,36,46,56 * * * *');
$this->scheduleCommand('activity:status-count')->cron('6,16,26,36,46,56 * * * *');
$this->scheduleCommand('mailbox:sync')->cron('6,16,26,36,46,56 * * * *');
$this->scheduleCommand('crm:reset-governor')->everyTenMinutes();
}
protected function scheduleEveryFifteenMinutes(): void
{
$this->scheduleCommand('datadog:report:processing-sla-activities')->everyFifteenMinutes();
$this->scheduleCommand('calendar:sync', ['--dateMode=daily'], 14)->cron('13,28,43,58 * * * *');
$this->scheduleCommand('activity:aircall:check-and-renew')->cron('9,24,39,54 * * * *');
$this->scheduleCommand('track:retry-failed-downloads')->cron('9,24,39,54 * * * *');
$this->scheduleCommand('crm:autolog-delayed')->cron('3,18,33,48 * * * *');
$this->scheduleCommand('activity:sync', [
'--from' => now()->subMinutes(16)->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--skipProviders' => [
Activity::PROVIDER_RINGCENTRAL,
Activity::PROVIDER_AVAYA,
Activity::PROVIDER_TELUS,
Activity::PROVIDER_TALKDESK,
],
])->everyFifteenMinutes();
$this->scheduleCommand('activity:sync', [
Activity::PROVIDER_RINGCENTRAL,
Activity::PROVIDER_AVAYA,
Activity::PROVIDER_TELUS,
Activity::PROVIDER_TALKDESK,
'--from' => now()->subMinutes(16)->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
])->cron('7,22,37,52 * * * *');
}
protected function scheduleEveryThirtyMinutes(): void
{
$this->scheduleCommand('crm:sync-objects')->cron('14,44 * * * *');
$this->scheduleCommand('mailbox:batch:fail-stalled')->everyThirtyMinutes();
$this->scheduleCommand('activities:delete-activities-for-deactivated-teams', expiresAt: 5)
->between('02:58', '05:29')
->everyThirtyMinutes()
->runInBackground();
$this->scheduleActivitiesHardDelete();
}
protected function scheduleHourly(): void
{
$this->scheduleCommand('jiminny:transcription:retry-stuck')->hourly();
$this->scheduleCommand('twilio:recover-tracks')->cron('22 * * * *');
$this->scheduleCommand('dialers:sync-users')->cron('22 * * * *');
$this->scheduleCommand('datadog:report:failed-processing-states')->cron('22 * * * *');
$this->scheduleCommand('automated-reports:send')->hourly();
$this->scheduleCommand('deal-insights:send-update')->hourlyAt(0);
$this->scheduleCommand('crm:integration-app-validate-team-connection')->hourlyAt(23);
}
protected function scheduleDaily(): void
{
$this->scheduleCommand('teams:sync-planhat')->daily();
$this->scheduleCommand('twilio:sync-addresses')->daily();
$this->scheduleCommand('twilio:sync-zone-access')->daily();
$this->scheduleCommand('mailbox:text-relay:watch-text-events')->daily();
$this->scheduleCommand('users:sync-licence-data')->daily();
$this->scheduleCommand('users:sync-intercom-data')->daily();
}
protected function scheduleWeekly(int $currentDay): void
{
if ($currentDay === 0) {
$this->scheduleCommand('crm:update-opp-specs')->weeklyOn(0);
}
if ($currentDay === 6) {
$this->scheduleCommand('jiminny:acl:remove-expired-role-change-events')->saturdays();
$this->scheduleCommand('activity:sync', [
Activity::PROVIDER_AMAZON_CONNECT,
'--from' => now()->subDays(7)->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
])->saturdays()->at('01:00')->runInBackground();
$this->scheduleCommand('calendar:event:delete-past', ['--force'], 60)
->saturdays()->at('01:07')->runInBackground();
$this->scheduleCommand('calendar:event:delete-cancelled', ['--force'], 60 * 47 + 52)
->saturdays()->at('05:08')->runInBackground();
$this->scheduleCommand('nudges-data-clean-up --squashNudgeRuns')
->weeklyOn(6, '6:00');
}
}
protected function scheduleSpecificTimes(): void
{
$this->scheduleCommand('deal-risks:calculate', ['--cronjob'])->dailyAt('00:00');
$this->scheduleCommand(DeleteInboxEmailsCommand::NAME, [
'--status' => InboxEmail::STATUS_DISCARDED,
'--to' => now()->subWeeks(2)->format('Y-m-d'),
])->saturdays()->at('00:20')->runInBackground();
$this->scheduleCommand(DeleteInboxEmailsCommand::NAME, [
'--status' => InboxEmail::STATUS_PROCESSED,
'--to' => now()->subWeeks(2)->format('Y-m-d'),
])->saturdays()->at('00:30')->runInBackground();
$this->scheduleCommand('automated-reports')->dailyAt('01:00');
$this->scheduleCommand('crm:sync-team-metadata')->dailyAt('01:05');
$this->scheduleCommand('crm:sync-profile-metadata')->dailyAt('01:05');
$this->scheduleCommand('calendar:sync-deleted-events')->dailyAt('01:10');
$this->scheduleCommand('teams:delete-retention')->dailyAt('02:55');
$this->scheduleCommand('teams:delete-deactivated')->dailyAt('02:58');
$this->scheduleCommand('twilio:remote-lifecycle')->dailyAt('03:00');
$this->scheduleCommand('activity:sync', [
Activity::PROVIDER_VONAGE,
Activity::PROVIDER_FIVE_NINE,
'--from' => now()->subDay()->startOfDay()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->subDay()->endOfDay()->format(SyncActivity::ALLOWED_DATE_FORMAT),
])->dailyAt('03:05');
$this->scheduleCommand('activities:delete-retention-teams', expiresAt: 240)->dailyAt('03:04');
$this->scheduleCommand('automated-reports:run-retention-policy', expiresAt: 120)->dailyAt('03:15');
$this->scheduleCommand('stop:hanging:livestreams')->dailyAt('03:30');
$this->scheduleCommand('crm:purge-sync-batches')->dailyAt('03:45');
$this->scheduleCommand('twilio:sync-numbers')->dailyAt('04:00');
if (! $this->app->environment('production')) {
$this->scheduleCommand('activities:hard-delete', ['--limit' => 1000, '--jobs' => 5], 60)
->dailyAt('04:02')->runInBackground();
}
$this->scheduleCommand('crm:full-sync-opportunity')->dailyAt('05:00');
$this->scheduleCommand('activity:sync', [
'--from' => now()->subDay()->startOfDay()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->subDay()->endOfDay()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--skipProviders' => [
Activity::PROVIDER_VONAGE,
Activity::PROVIDER_FIVE_NINE,
],
])->dailyAt('05:05');
if (! $this->app->environment('qa')) {
$this->scheduleCommand('ai-crm-notes:delete-old')->dailyAt('07:00');
}
$this->scheduleCommand('activity:sync-dispositions', [
Activity::PROVIDER_HUBSPOT,
'--from' => now()->subDay()->startOfDay()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->subDay()->endOfDay()->format(SyncActivity::ALLOWED_DATE_FORMAT),
])->dailyAt('07:05');
}
protected function scheduleDynamic(int $currentMinute): void
{
$this->scheduleHourlyFallbackActivitySyncs($currentMinute);
$this->scheduleBullhornHeartbeat($currentMinute);
}
private function scheduleHourlyFallbackActivitySyncs(int $offsetMinute): void
{
if ($offsetMinute === 0) {
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_CLOUD_TALK, 3, 0);
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_VONAGE, 6, 0);
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_CLOUDCALL, 3, 0);
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_CLOUDCALL_US, 3, 0);
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_FIVE_NINE, 3, 0);
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_HUBSPOT, 1, 0);
} elseif ($offsetMinute === 1) {
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_RINGCENTRAL, Constants::RINGCENTRAL_CALL_LOG_LOOK_BACK_HOURS, 1);
} elseif ($offsetMinute === 2) {
$this->scheduleHourlyFallbackActivitySync(Activity::PROVIDER_AVAYA, Constants::RINGCENTRAL_CALL_LOG_LOOK_BACK_HOURS, 2);
}
}
private function scheduleBullhornHeartbeat(int $currentMinute): void
{
$bhHeartbeatInterval = config('services.bullhorn.heartbeatInterval', 0);
if ($bhHeartbeatInterval > 0) {
$minutes = max((int) floor($bhHeartbeatInterval / 60), 1);
if ($currentMinute % $minutes === 0) {
$bhEvent = $this->scheduleCommand('crm:bullhorn:ping', ['--heartbeat']);
if ($minutes > 30) {
$bhEvent->hourly();
} else {
$bhEvent->cron(sprintf('*/%d * * * *', $minutes));
}
}
}
}
private function scheduleActivitiesHardDelete(): void
{
if (config(key: 'jiminny.deploy_region') === 'eu') {
$this->scheduleCommand(
name: 'activities:hard-delete',
options: ['--limit' => 1000, '--jobs' => 20],
expiresAt: 29
)
->between('02:59', '07:02')->everyThirtyMinutes()
->runInBackground();
} elseif ($this->app->environment('production')) {
$this->scheduleCommand(
name: 'activities:hard-delete',
options: ['--limit' => 2000, '--jobs' => 20],
expiresAt: 29
)
->between('02:59', '07:02')->everyThirtyMinutes()
->runInBackground();
}
}
private function scheduleHourlyFallbackActivitySync(string $provider, int $hours, int $offsetMinute = 0): void
{
$this->scheduleCommand('activity:sync', [
$provider,
'--from' => now()->subHours($hours)->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
'--to' => now()->startOfMinute()->format(SyncActivity::ALLOWED_DATE_FORMAT),
])->hourlyAt($offsetMinute);
}
/**
* Register the Closure based commands for the application.
*/
protected function commands(): void
{
require_once base_path('routes/console.php');
}
private function scheduleCommand(string $name, array $options = [], $expiresAt = 60 * 3): Event
{
return $this->schedule
->command($name, $options)
->withoutOverlapping($expiresAt)
->onOneServer()
->sendOutputTo($this->output)
;
}
}
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...
|
PhpStorm
|
faVsco.js – Kernel.php
|
NULL
|
76715
|
|
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
NULL
|
76716
|
|
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
NULL
|
76717
|
|
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
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Jiminny\Jobs\AutomatedReports\SendReportJob;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command as CommandAlias;
class AutomatedReportsSendCommand extends Command
{
private const string LOG_PREFIX = '[automated-reports:send]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports:send
{--result-id= : Force send a specific AutomatedReportResult by ID, bypassing the scheduled time check}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends automated reports based on user timezone';
public function __construct(
private readonly LoggerInterface $logger,
private readonly AutomatedReportsRepository $reportRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly BusDispatcher $dispatcher,
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$resultId = $this->option('result-id');
if ($resultId !== null) {
return $this->handleForceSend((int) $resultId);
}
$reportResults = $this->reportRepository->getGeneratedNotSentResults();
foreach ($reportResults as $reportResult) {
/** @var AutomatedReportResult $reportResult */
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if ($this->automatedReportsService->shouldSendReport($validRecipients, $reportResult->getGeneratedAt())) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching job', [
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
}
}
return CommandAlias::SUCCESS;
}
private function handleForceSend(int $resultId): int
{
$reportResult = AutomatedReportResult::find($resultId);
if ($reportResult === null) {
$this->logger->error(self::LOG_PREFIX . ' Result not found', ['result_id' => $resultId]);
return CommandAlias::FAILURE;
}
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if (empty($validRecipients)) {
$this->logger->error(self::LOG_PREFIX . ' No valid recipients found', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
return CommandAlias::FAILURE;
}
$this->logger->info(self::LOG_PREFIX . ' Force dispatching job', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
return CommandAlias::SUCCESS;
}
}
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsSendCommand.php
|
NULL
|
76718
|
|
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
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Jiminny\Jobs\AutomatedReports\SendReportJob;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command as CommandAlias;
class AutomatedReportsSendCommand extends Command
{
private const string LOG_PREFIX = '[automated-reports:send]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports:send
{--result-id= : Force send a specific AutomatedReportResult by ID, bypassing the scheduled time check}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends automated reports based on user timezone';
public function __construct(
private readonly LoggerInterface $logger,
private readonly AutomatedReportsRepository $reportRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly BusDispatcher $dispatcher,
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$resultId = $this->option('result-id');
if ($resultId !== null) {
return $this->handleForceSend((int) $resultId);
}
$reportResults = $this->reportRepository->getGeneratedNotSentResults();
foreach ($reportResults as $reportResult) {
/** @var AutomatedReportResult $reportResult */
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if ($this->automatedReportsService->shouldSendReport($validRecipients, $reportResult->getGeneratedAt())) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching job', [
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
}
}
return CommandAlias::SUCCESS;
}
private function handleForceSend(int $resultId): int
{
$reportResult = AutomatedReportResult::find($resultId);
if ($reportResult === null) {
$this->logger->error(self::LOG_PREFIX . ' Result not found', ['result_id' => $resultId]);
return CommandAlias::FAILURE;
}
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if (empty($validRecipients)) {
$this->logger->error(self::LOG_PREFIX . ' No valid recipients found', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
return CommandAlias::FAILURE;
}
$this->logger->info(self::LOG_PREFIX . ' Force dispatching job', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
return CommandAlias::SUCCESS;
}
}
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsSendCommand.php
|
NULL
|
76719
|
|
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
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Jiminny\Jobs\AutomatedReports\SendReportJob;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command as CommandAlias;
class AutomatedReportsSendCommand extends Command
{
private const string LOG_PREFIX = '[automated-reports:send]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports:send
{--result-id= : Force send a specific AutomatedReportResult by ID, bypassing the scheduled time check}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends automated reports based on user timezone';
public function __construct(
private readonly LoggerInterface $logger,
private readonly AutomatedReportsRepository $reportRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly BusDispatcher $dispatcher,
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$resultId = $this->option('result-id');
if ($resultId !== null) {
return $this->handleForceSend((int) $resultId);
}
$reportResults = $this->reportRepository->getGeneratedNotSentResults();
foreach ($reportResults as $reportResult) {
/** @var AutomatedReportResult $reportResult */
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if ($this->automatedReportsService->shouldSendReport($validRecipients, $reportResult->getGeneratedAt())) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching job', [
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
}
}
return CommandAlias::SUCCESS;
}
private function handleForceSend(int $resultId): int
{
$reportResult = AutomatedReportResult::find($resultId);
if ($reportResult === null) {
$this->logger->error(self::LOG_PREFIX . ' Result not found', ['result_id' => $resultId]);
return CommandAlias::FAILURE;
}
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if (empty($validRecipients)) {
$this->logger->error(self::LOG_PREFIX . ' No valid recipients found', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
return CommandAlias::FAILURE;
}
$this->logger->info(self::LOG_PREFIX . ' Force dispatching job', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
return CommandAlias::SUCCESS;
}
}
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsSendCommand.php
|
NULL
|
76720
|
|
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
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Jiminny\Jobs\AutomatedReports\SendReportJob;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command as CommandAlias;
class AutomatedReportsSendCommand extends Command
{
private const string LOG_PREFIX = '[automated-reports:send]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports:send
{--result-id= : Force send a specific AutomatedReportResult by ID, bypassing the scheduled time check}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends automated reports based on user timezone';
public function __construct(
private readonly LoggerInterface $logger,
private readonly AutomatedReportsRepository $reportRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly BusDispatcher $dispatcher,
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$resultId = $this->option('result-id');
if ($resultId !== null) {
return $this->handleForceSend((int) $resultId);
}
$reportResults = $this->reportRepository->getGeneratedNotSentResults();
foreach ($reportResults as $reportResult) {
/** @var AutomatedReportResult $reportResult */
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if ($this->automatedReportsService->shouldSendReport($validRecipients, $reportResult->getGeneratedAt())) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching job', [
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
}
}
return CommandAlias::SUCCESS;
}
private function handleForceSend(int $resultId): int
{
$reportResult = AutomatedReportResult::find($resultId);
if ($reportResult === null) {
$this->logger->error(self::LOG_PREFIX . ' Result not found', ['result_id' => $resultId]);
return CommandAlias::FAILURE;
}
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if (empty($validRecipients)) {
$this->logger->error(self::LOG_PREFIX . ' No valid recipients found', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
return CommandAlias::FAILURE;
}
$this->logger->info(self::LOG_PREFIX . ' Force dispatching job', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
return CommandAlias::SUCCESS;
}
}
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsSendCommand.php
|
NULL
|
76721
|
|
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
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Jiminny\Jobs\AutomatedReports\SendReportJob;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command as CommandAlias;
class AutomatedReportsSendCommand extends Command
{
private const string LOG_PREFIX = '[automated-reports:send]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports:send
{--result-id= : Force send a specific AutomatedReportResult by ID, bypassing the scheduled time check}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends automated reports based on user timezone';
public function __construct(
private readonly LoggerInterface $logger,
private readonly AutomatedReportsRepository $reportRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly BusDispatcher $dispatcher,
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$resultId = $this->option('result-id');
if ($resultId !== null) {
return $this->handleForceSend((int) $resultId);
}
$reportResults = $this->reportRepository->getGeneratedNotSentResults();
foreach ($reportResults as $reportResult) {
/** @var AutomatedReportResult $reportResult */
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if ($this->automatedReportsService->shouldSendReport($validRecipients, $reportResult->getGeneratedAt())) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching job', [
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
}
}
return CommandAlias::SUCCESS;
}
private function handleForceSend(int $resultId): int
{
$reportResult = AutomatedReportResult::find($resultId);
if ($reportResult === null) {
$this->logger->error(self::LOG_PREFIX . ' Result not found', ['result_id' => $resultId]);
return CommandAlias::FAILURE;
}
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if (empty($validRecipients)) {
$this->logger->error(self::LOG_PREFIX . ' No valid recipients found', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
return CommandAlias::FAILURE;
}
$this->logger->info(self::LOG_PREFIX . ' Force dispatching job', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
return CommandAlias::SUCCESS;
}
}
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsSendCommand.php
|
NULL
|
76722
|
|
DMSActivityMorerireroxToolsHelpcalMistorbookmarksJ DMSActivityMorerireroxToolsHelpcalMistorbookmarksJiminny …..vXStarredi• jiminny-x-integrati..8 platform-inner-teamE) Channels# ai-chapter# ai-team# alerts# backend# c-learning-peoplei confusion-clinic# curiosity_labadeal-insichts-dev# engineering# frontend# general# infra-changes# jiminny-bg8 people-with-copilo...8 people-with-zoom-# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the Deople of iimi...ProtllesWindow& platform-inner-...& 10MessagesChannel OverviewMoreYesterdayjinnylaop Aor 22nd Added by GitHubNikolay Ivanov 3:24 PMнякой нещо да е настроивал по githubactions. Почна да прави къмити вместо менбез да съм му разрешевал?https:/github.com/lminnv/app/pull/1200//changes/a68f42f210859f838a4fdced451f750627besoioИли нещо аз не разбирам?0AA0e 20 replies Last reply 18...Nikolay Yankov 3:50PMreplied to a uhread: някои нешо ла е насто..лол. ами предлагам маи ла му заораним лапускам към всички ла вилятNikolav Yankov 9.38 AMЩе се забавя за дейлито. Започнете без Мен.Aneliva Angelova 9:43 AMIДобро утро, няма да успея да вляза влейлито. Пествам ньлжовете.Message & platform-inner-team+ Aa I..•) New TabAl reports promotion pages by nik• JY-9712 | Nuges to expire after on8 Jiminnyu Userpilot Logged-activityJY-20157 add not enough activ XPipelines - jiminny/app+ New Tab©github.com/jimjiminny / app 8<> Code87 Pull requests 31( Agents |© Actions•• Wiki © Security and quality 32 ~ Insights 3 Settings@ On April 24 we'll start using GitHub Copilot interaction data for Al model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.JY-20157 add not enough activities notification #12011 •$1 Open LakyLak wants to merge 2 commits into master from JY-20157-AJ-report-not-send-notification@) Conversation o• Commits 2|- Checks 21E Files changed 13A © All commits +Q Filter files...apo/Console/Commands/Reports/AutomatedReportsCommand.ohp@ -61,21 +61,29 @ public function handle(): intv = Console/Commands/Renorts|Snow = Carbon: : now();E AutomatedReportsCommand...v Jobs/AutomatedReportsE RequestGenerateAskJiminnyR...SendReportNotGeneratedMail...v @ Listeners/AutomatedReports/U….E TrackAutomatedReportGener...v # Mail/ReportsS1sMondav = Snow->1SMonday)S1sr1rstDayUtMonth = Snow->day === 1;ScurrentMonth = Snow->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', [+ ReportNotGenerated.ohp |"1SMonday' => S1SMonday,~ E Services/Kiosk/AutomatedRepo…..AskJiminnyReportActivityServ…'isFirstDay0fMonth' => SisFirstDay0fMonth,'currentMonth' => ScurrentMonth.E AutomatedReportsService.php'isQuarterlyMonth' => SisQuarterlyMonth,~E resources/views/emails/reportsreport-not-generated.blade.php/I Process dailv revortsl• F tests/UnitSthis->processReports(AutomatedReportsService::FREQUENCYDAILY):~ Jobs/AutomatedReportsE ReguestGenerateAsk JiminnvR....v = listeners/AutomatedRenorts/U.₴ TrackAutomatedReportGener..v E Services/Kiosk/AutomatedRepo…..E AskJiminnyReportActivityServ....AutomatedReportsServiceActi…./ Process weekly renorts on Mondavcif (SisMondav) {64 +67 +74 +86 +@40@ Daily - Platform - nowQ Type to search100% C4 8• Fri 24 Apr 9:46:13• Checks pending Code • (Preview) -+384 -52 9000C 0 I 13 viewedSubmit review+10 -2 mane [ Viewed0 ...Snow = Carhon:.nowdSisMondav = Snow->1SMonday)"Sisweekend = $now->isWeekend():SisFirstDay0fMonth = Snow->day === 1;ScurrentMonth = Snow->month.SisManualTrigger = $this->option('report-id') !== null;// Check if the current month is a quarterly month (January, April, July, October)$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);Sthis->loager->info(self::L0G PREFIX . ' Checkina conditions'. [I"isMonday' => SisMonday,'isweekend' => $isWeekend,'isFirstDay0fMonth' => $isFirstDay0fMonth,'currentMonth' => ScurrentMonth.l'isQuarterlyMonth' => SisQuarterlyMonth,/ Process dailv renorts on weekdavs onlv (skio Saturdav/Sundav)...// Manual triggers via --report-id bypass the weekend skip.if (I Sisweekend || SisManualTriager) {Sthis->processReports(AutomatedReportsService::FREQUENCY_DAILY):} else {ISthis->logger->info(self::L0G PREFIX . ' Skipping daily reports on weekend'):/ Process weekly renorts on Mondavslif (SisMonday) {...
|
PhpStorm
|
|
NULL
|
76723
|
|
Firefox• 0FileEditViewHistory→BookmarksProfilesToo Firefox• 0FileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com•Daily - Platform - now100% K78 • Fri 24 Apr 9:46:13|=Pop out this videoNikolay NikolovStefka StoyanovaGalya DimitrovaLukas Kovalik9:46 AM | Daily - Platform• 0:27...
|
PhpStorm
|
|
NULL
|
76724
|
|
DMSActivityMorerireroxToolsHelpcalMistorbookmarksJ DMSActivityMorerireroxToolsHelpcalMistorbookmarksJiminny …..vXStarredi• jiminny-x-integrati..8 platform-inner-teamE) Channels# ai-chapter# ai-team# alerts# backend# c-learning-peoplei confusion-clinic# curiosity_labadeal-insichts-dev# engineering# frontend# general# infra-changes# jiminny-bg8 people-with-copilo...8 people-with-zoom-# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the Deople of iimi...ProtllesWindow& platform-inner-...& 10MessagesChannel OverviewMoreYesterdayjinnylaop Aor 22nd Added by GitHubNikolay Ivanov 3:24 PMнякой нещо да е настроивал по githubactions. Почна да прави къмити вместо менбез да съм му разрешевал?https:/github.com/lminnv/app/pull/1200//changes/a68f42f210859f838a4fdced451f750627besoioИли нещо аз не разбирам?0AA0e 20 replies Last reply 18...Nikolay Yankov 3:50PMreplied to a uhread: някои нешо ла е насто..лол. ами предлагам маи ла му заораним лапускам към всички ла вилятNikolav Yankov 9.38 AMЩе се забавя за дейлито. Започнете без Мен.Aneliva Angelova 9:43 AMIДобро утро, няма да успея да вляза влейлито. Пествам ньлжовете.Message & platform-inner-team+ Aa I..•) New TabAl reports promotion pages by nik• JY-9712 | Nuges to expire after on8 Jiminnyu Userpilot Logged-activityJY-20157 add not enough activ XPipelines - jiminny/app+ New Tab©github.com/jimjiminny / app 8<> Code87 Pull requests 31( Agents |© Actions•• Wiki © Security and quality 32 ~ Insights 3 Settings@ On April 24 we'll start using GitHub Copilot interaction data for Al model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.JY-20157 add not enough activities notification #12011 •$1 Open LakyLak wants to merge 2 commits into master from JY-20157-AJ-report-not-send-notification@) Conversation o• Commits 2|- Checks 21E Files changed 13A © All commits +Q Filter files...apo/Console/Commands/Reports/AutomatedReportsCommand.ohp@ -61,21 +61,29 @ public function handle(): intv = Console/Commands/Renorts|Snow = Carbon: : now();E AutomatedReportsCommand...v Jobs/AutomatedReportsE RequestGenerateAskJiminnyR...SendReportNotGeneratedMail...v @ Listeners/AutomatedReports/U….E TrackAutomatedReportGener...v # Mail/ReportsS1sMondav = Snow->1SMonday)S1sr1rstDayUtMonth = Snow->day === 1;ScurrentMonth = Snow->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', [+ ReportNotGenerated.ohp |"1SMonday' => S1SMonday,~ E Services/Kiosk/AutomatedRepo…..AskJiminnyReportActivityServ…'isFirstDay0fMonth' => SisFirstDay0fMonth,'currentMonth' => ScurrentMonth.E AutomatedReportsService.php'isQuarterlyMonth' => SisQuarterlyMonth,~E resources/views/emails/reportsreport-not-generated.blade.php/I Process dailv revortsl• F tests/UnitSthis->processReports(AutomatedReportsService::FREQUENCYDAILY):~ Jobs/AutomatedReportsE ReguestGenerateAsk JiminnvR....v = listeners/AutomatedRenorts/U.₴ TrackAutomatedReportGener..v E Services/Kiosk/AutomatedRepo…..E AskJiminnyReportActivityServ....AutomatedReportsServiceActi…./ Process weekly renorts on Mondavcif (SisMondav) {64 +67 +74 +86 +@40@ Daily - Platform - nowQ Type to search100% C4 8• Fri 24 Apr 9:46:13• Checks pending Code • (Preview) -+384 -52 9000C 0 I 13 viewedSubmit review+10 -2 mane [ Viewed0 ...Snow = Carhon:.nowdSisMondav = Snow->1SMonday)"Sisweekend = $now->isWeekend():SisFirstDay0fMonth = Snow->day === 1;ScurrentMonth = Snow->month.SisManualTrigger = $this->option('report-id') !== null;// Check if the current month is a quarterly month (January, April, July, October)$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);Sthis->loager->info(self::L0G PREFIX . ' Checkina conditions'. [I"isMonday' => SisMonday,'isweekend' => $isWeekend,'isFirstDay0fMonth' => $isFirstDay0fMonth,'currentMonth' => ScurrentMonth.l'isQuarterlyMonth' => SisQuarterlyMonth,/ Process dailv renorts on weekdavs onlv (skio Saturdav/Sundav)...// Manual triggers via --report-id bypass the weekend skip.if (I Sisweekend || SisManualTriager) {Sthis->processReports(AutomatedReportsService::FREQUENCY_DAILY):} else {ISthis->logger->info(self::L0G PREFIX . ' Skipping daily reports on weekend'):/ Process weekly renorts on Mondavslif (SisMonday) {...
|
PhpStorm
|
|
NULL
|
76725
|
|
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
8
1
7
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Traits\RequiresUUID;
/**
* Jiminny\Models\AutomatedReport
*
* @property int $id
* @property string $uuid
* @property int $team_id
* @property string $type
* @property bool $status
* @property string $frequency
* @property Carbon|null $from
* @property Carbon|null $to
* @property int|null $deal_value_min
* @property int|null $deal_value_max
* @property array $call_types
* @property array $media_types
* @property int|null $call_duration_min
* @property int|null $call_duration_max
* @property array|null $groups
* @property array|null $playbook_categories
* @property array|null $deal_at_call_stages
* @property array|null $current_deal_stages
* @property array $recipients
* @property string|null $additional_prompt_input
* @property string|null $custom_name
* @property int|null $activity_search_id
* @property int|null $ask_anything_prompt_id
* @property Carbon|null $expires_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property-read \Jiminny\Models\Team $team
* @property-read \Jiminny\Models\Activity\Search|null $savedSearch
* @property-read \Jiminny\Models\AskAnything\AskAnythingPrompt|null $askAnythingPrompt
*/
class AutomatedReport extends Model
{
use RequiresUUID;
use SoftDeletes;
protected $table = 'automated_reports';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'team_id',
'type',
'status',
'frequency',
'from',
'to',
'deal_value_min',
'deal_value_max',
'call_types',
'media_types',
'call_duration_min',
'call_duration_max',
'groups',
'playbook_categories',
'deal_at_call_stages',
'current_deal_stages',
'recipients',
'jiminny_recipients',
'additional_prompt_input',
'custom_name',
'created_by',
'activity_search_id',
'ask_anything_prompt_id',
'expires_at',
];
protected $hidden = ['uuid'];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'status' => 'boolean',
'from' => 'datetime',
'to' => 'datetime',
'call_types' => 'array',
'media_types' => 'array',
'groups' => 'array',
'playbook_categories' => 'array',
'deal_at_call_stages' => 'array',
'current_deal_stages' => 'array',
'recipients' => 'array',
'jiminny_recipients' => 'array',
'expires_at' => 'date',
'deleted_at' => 'datetime',
];
}
/**
* Get the team that owns the automated report.
*/
public function team()
{
return $this->belongsTo(Team::class);
}
/**
*
* Get the user who created the report.
*/
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function savedSearch(): BelongsTo
{
return $this->belongsTo(Search::class, 'activity_search_id');
}
public function askAnythingPrompt(): BelongsTo
{
return $this->belongsTo(AskAnythingPrompt::class, 'ask_anything_prompt_id');
}
public function isAskJiminnyReport(): bool
{
return $this->getType() === AutomatedReportsService::TYPE_ASK_JIMINNY;
}
public function isExpired(): bool
{
$expiresAt = $this->getExpiresAt();
return $expiresAt !== null && $expiresAt->isPast();
}
public function getActivitySearchId(): ?int
{
return $this->getAttribute('activity_search_id');
}
public function getAskAnythingPromptId(): ?int
{
return $this->getAttribute('ask_anything_prompt_id');
}
public function getExpiresAt(): ?Carbon
{
return $this->getAttribute('expires_at');
}
public function getSavedSearch(): ?Search
{
return $this->getAttribute('savedSearch');
}
public function getAskAnythingPrompt(): ?AskAnythingPrompt
{
return $this->getAttribute('askAnythingPrompt');
}
/**
* Get the ID of the automated report.
*
* @return int
*/
public function getId(): int
{
return $this->getAttribute('id');
}
/**
* Get the UUID of the automated report.
*
* @return string
*/
public function getUuid(): string
{
return $this->getAttribute('id_string');
}
/**
* Get the team ID of the automated report.
*
* @return int
*/
public function getTeamId(): int
{
return $this->getAttribute('team_id');
}
/**
* Get the type of the automated report.
*
* @return string
*/
public function getType(): string
{
return $this->getAttribute('type');
}
/**
* Get the status of the automated report.
* True means active, false means inactive.
*
* @return bool
*/
public function getStatus(): bool
{
return $this->getAttribute('status');
}
/**
* Get the frequency of the automated report.
*
* @return string
*/
public function getFrequency(): string
{
return $this->getAttribute('frequency');
}
/**
* Get the from date of the automated report.
*
* @return Carbon|null
*/
public function getFrom(): ?Carbon
{
return $this->getAttribute('from');
}
/**
* Get the to date of the automated report.
*
* @return Carbon|null
*/
public function getTo(): ?Carbon
{
return $this->getAttribute('to');
}
/**
* Get the minimum deal value of the automated report.
*
* @return int|null
*/
public function getDealValueMin(): ?int
{
return $this->getAttribute('deal_value_min');
}
/**
* Get the maximum deal value of the automated report.
*
* @return int|null
*/
public function getDealValueMax(): ?int
{
return $this->getAttribute('deal_value_max');
}
/**
* Get the call types of the automated report.
*
* @return array
*/
public function getCallTypes(): array
{
return $this->getAttribute('call_types') ?? [];
}
public function getMediaTypes(): array
{
return $this->getAttribute('media_types') ?? [];
}
/**
* Get the minimum call duration of the automated report.
*
* @return int|null
*/
public function getCallDurationMin(): ?int
{
return $this->getAttribute('call_duration_min');
}
/**
* Get the maximum call duration of the automated report.
*
* @return int|null
*/
public function getCallDurationMax(): ?int
{
return $this->getAttribute('call_duration_max');
}
/**
* Get the groups of the automated report.
*
* @return array
*/
public function getGroups(): array
{
return $this->getAttribute('groups') ?? [];
}
/**
* Get the playbook categories of the automated report.
*
* @return array
*/
public function getPlaybookCategories(): array
{
return $this->getAttribute('playbook_categories') ?? [];
}
/**
* Get the deal at call stages of the automated report.
*
* @return array
*/
public function getDealAtCallStages(): array
{
return $this->getAttribute('deal_at_call_stages') ?? [];
}
/**
* Get the current deal stages of the automated report.
*
* @return array
*/
public function getCurrentDealStages(): array
{
return $this->getAttribute('current_deal_stages') ?? [];
}
/**
* Get the recipients of the automated report.
*
* @return array
*/
public function getRecipients(): array
{
return $this->getAttribute('recipients') ?? [];
}
/**
* Get the Jiminny's recipients of the automated report.
*
* @return array
*/
public function getJiminnyRecipients(): array
{
return $this->getAttribute('jiminny_recipients') ?? [];
}
/**
* Get the additional prompt input of the automated report.
*
* @return string|null
*/
public function getAdditionalPromptInput(): ?string
{
return $this->getAttribute('additional_prompt_input');
}
public function getCustomName(): ?string
{
return $this->getAttribute('custom_name');
}
/**
* Get the created at date of the automated report.
*
* @return Carbon
*/
public function getCreatedAt(): Carbon
{
return $this->getAttribute('created_at');
}
/**
* Get the updated at date of the automated report.
*
* @return Carbon
*/
public function getUpdatedAt(): Carbon
{
return $this->getAttribute('updated_at');
}
/**
* Get the deleted at date of the automated report.
*
* @return Carbon|null
*/
public function getDeletedAt(): ?Carbon
{
return $this->getAttribute('deleted_at');
}
public function getTeam(): Team
{
return $this->getAttribute('team');
}
public function getCreator(): ?User
{
return $this->getAttribute('creator');
}
}
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...
|
PhpStorm
|
faVsco.js – AutomatedReport.php
|
NULL
|
76726
|
|
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
8
1
7
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Traits\RequiresUUID;
/**
* Jiminny\Models\AutomatedReport
*
* @property int $id
* @property string $uuid
* @property int $team_id
* @property string $type
* @property bool $status
* @property string $frequency
* @property Carbon|null $from
* @property Carbon|null $to
* @property int|null $deal_value_min
* @property int|null $deal_value_max
* @property array $call_types
* @property array $media_types
* @property int|null $call_duration_min
* @property int|null $call_duration_max
* @property array|null $groups
* @property array|null $playbook_categories
* @property array|null $deal_at_call_stages
* @property array|null $current_deal_stages
* @property array $recipients
* @property string|null $additional_prompt_input
* @property string|null $custom_name
* @property int|null $activity_search_id
* @property int|null $ask_anything_prompt_id
* @property Carbon|null $expires_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property-read \Jiminny\Models\Team $team
* @property-read \Jiminny\Models\Activity\Search|null $savedSearch
* @property-read \Jiminny\Models\AskAnything\AskAnythingPrompt|null $askAnythingPrompt
*/
class AutomatedReport extends Model
{
use RequiresUUID;
use SoftDeletes;
protected $table = 'automated_reports';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'team_id',
'type',
'status',
'frequency',
'from',
'to',
'deal_value_min',
'deal_value_max',
'call_types',
'media_types',
'call_duration_min',
'call_duration_max',
'groups',
'playbook_categories',
'deal_at_call_stages',
'current_deal_stages',
'recipients',
'jiminny_recipients',
'additional_prompt_input',
'custom_name',
'created_by',
'activity_search_id',
'ask_anything_prompt_id',
'expires_at',
];
protected $hidden = ['uuid'];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'status' => 'boolean',
'from' => 'datetime',
'to' => 'datetime',
'call_types' => 'array',
'media_types' => 'array',
'groups' => 'array',
'playbook_categories' => 'array',
'deal_at_call_stages' => 'array',
'current_deal_stages' => 'array',
'recipients' => 'array',
'jiminny_recipients' => 'array',
'expires_at' => 'date',
'deleted_at' => 'datetime',
];
}
/**
* Get the team that owns the automated report.
*/
public function team()
{
return $this->belongsTo(Team::class);
}
/**
*
* Get the user who created the report.
*/
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function savedSearch(): BelongsTo
{
return $this->belongsTo(Search::class, 'activity_search_id');
}
public function askAnythingPrompt(): BelongsTo
{
return $this->belongsTo(AskAnythingPrompt::class, 'ask_anything_prompt_id');
}
public function isAskJiminnyReport(): bool
{
return $this->getType() === AutomatedReportsService::TYPE_ASK_JIMINNY;
}
public function isExpired(): bool
{
$expiresAt = $this->getExpiresAt();
return $expiresAt !== null && $expiresAt->isPast();
}
public function getActivitySearchId(): ?int
{
return $this->getAttribute('activity_search_id');
}
public function getAskAnythingPromptId(): ?int
{
return $this->getAttribute('ask_anything_prompt_id');
}
public function getExpiresAt(): ?Carbon
{
return $this->getAttribute('expires_at');
}
public function getSavedSearch(): ?Search
{
return $this->getAttribute('savedSearch');
}
public function getAskAnythingPrompt(): ?AskAnythingPrompt
{
return $this->getAttribute('askAnythingPrompt');
}
/**
* Get the ID of the automated report.
*
* @return int
*/
public function getId(): int
{
return $this->getAttribute('id');
}
/**
* Get the UUID of the automated report.
*
* @return string
*/
public function getUuid(): string
{
return $this->getAttribute('id_string');
}
/**
* Get the team ID of the automated report.
*
* @return int
*/
public function getTeamId(): int
{
return $this->getAttribute('team_id');
}
/**
* Get the type of the automated report.
*
* @return string
*/
public function getType(): string
{
return $this->getAttribute('type');
}
/**
* Get the status of the automated report.
* True means active, false means inactive.
*
* @return bool
*/
public function getStatus(): bool
{
return $this->getAttribute('status');
}
/**
* Get the frequency of the automated report.
*
* @return string
*/
public function getFrequency(): string
{
return $this->getAttribute('frequency');
}
/**
* Get the from date of the automated report.
*
* @return Carbon|null
*/
public function getFrom(): ?Carbon
{
return $this->getAttribute('from');
}
/**
* Get the to date of the automated report.
*
* @return Carbon|null
*/
public function getTo(): ?Carbon
{
return $this->getAttribute('to');
}
/**
* Get the minimum deal value of the automated report.
*
* @return int|null
*/
public function getDealValueMin(): ?int
{
return $this->getAttribute('deal_value_min');
}
/**
* Get the maximum deal value of the automated report.
*
* @return int|null
*/
public function getDealValueMax(): ?int
{
return $this->getAttribute('deal_value_max');
}
/**
* Get the call types of the automated report.
*
* @return array
*/
public function getCallTypes(): array
{
return $this->getAttribute('call_types') ?? [];
}
public function getMediaTypes(): array
{
return $this->getAttribute('media_types') ?? [];
}
/**
* Get the minimum call duration of the automated report.
*
* @return int|null
*/
public function getCallDurationMin(): ?int
{
return $this->getAttribute('call_duration_min');
}
/**
* Get the maximum call duration of the automated report.
*
* @return int|null
*/
public function getCallDurationMax(): ?int
{
return $this->getAttribute('call_duration_max');
}
/**
* Get the groups of the automated report.
*
* @return array
*/
public function getGroups(): array
{
return $this->getAttribute('groups') ?? [];
}
/**
* Get the playbook categories of the automated report.
*
* @return array
*/
public function getPlaybookCategories(): array
{
return $this->getAttribute('playbook_categories') ?? [];
}
/**
* Get the deal at call stages of the automated report.
*
* @return array
*/
public function getDealAtCallStages(): array
{
return $this->getAttribute('deal_at_call_stages') ?? [];
}
/**
* Get the current deal stages of the automated report.
*
* @return array
*/
public function getCurrentDealStages(): array
{
return $this->getAttribute('current_deal_stages') ?? [];
}
/**
* Get the recipients of the automated report.
*
* @return array
*/
public function getRecipients(): array
{
return $this->getAttribute('recipients') ?? [];
}
/**
* Get the Jiminny's recipients of the automated report.
*
* @return array
*/
public function getJiminnyRecipients(): array
{
return $this->getAttribute('jiminny_recipients') ?? [];
}
/**
* Get the additional prompt input of the automated report.
*
* @return string|null
*/
public function getAdditionalPromptInput(): ?string
{
return $this->getAttribute('additional_prompt_input');
}
public function getCustomName(): ?string
{
return $this->getAttribute('custom_name');
}
/**
* Get the created at date of the automated report.
*
* @return Carbon
*/
public function getCreatedAt(): Carbon
{
return $this->getAttribute('created_at');
}
/**
* Get the updated at date of the automated report.
*
* @return Carbon
*/
public function getUpdatedAt(): Carbon
{
return $this->getAttribute('updated_at');
}
/**
* Get the deleted at date of the automated report.
*
* @return Carbon|null
*/
public function getDeletedAt(): ?Carbon
{
return $this->getAttribute('deleted_at');
}
public function getTeam(): Team
{
return $this->getAttribute('team');
}
public function getCreator(): ?User
{
return $this->getAttribute('creator');
}
}
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...
|
PhpStorm
|
faVsco.js – AutomatedReport.php
|
NULL
|
76727
|
|
Firefox• 0FileEditViewHistory→BookmarksProfilesToo Firefox• 0FileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com•Daily - Platform - now100% K78 • Fri 24 Apr 9:46:13|=Pop out this videoNikolay NikolovStefka StoyanovaGalya DimitrovaLukas Kovalik9:46 AM | Daily - Platform• 0:27...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
NULL
|
76728
|
|
DMSActivityMorerireroxToolsHelpcalMistorbookmarksJ DMSActivityMorerireroxToolsHelpcalMistorbookmarksJiminny …..vXStarredi• jiminny-x-integrati..8 platform-inner-teamE) Channels# ai-chapter# ai-team# alerts# backend# c-learning-peoplei confusion-clinic# curiosity_labadeal-insichts-dev# engineering# frontend# general# infra-changes# jiminny-bg8 people-with-copilo...8 people-with-zoom-# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the Deople of iimi...ProtllesWindow& platform-inner-...& 10MessagesChannel OverviewMoreYesterdayjinnylaop Aor 22nd Added by GitHubNikolay Ivanov 3:24 PMнякой нещо да е настроивал по githubactions. Почна да прави къмити вместо менбез да съм му разрешевал?https:/github.com/lminnv/app/pull/1200//changes/a68f42f210859f838a4fdced451f750627besoioИли нещо аз не разбирам?0AA0e 20 replies Last reply 18...Nikolay Yankov 3:50PMreplied to a uhread: някои нешо ла е насто..лол. ами предлагам маи ла му заораним лапускам към всички ла вилятNikolav Yankov 9.38 AMЩе се забавя за дейлито. Започнете без Мен.Aneliva Angelova 9:43 AMIДобро утро, няма да успея да вляза влейлито. Пествам ньлжовете.Message & platform-inner-team+ Aa I..•) New TabAl reports promotion pages by nik• JY-9712 | Nuges to expire after on8 Jiminnyu Userpilot Logged-activityJY-20157 add not enough activ XPipelines - jiminny/app+ New Tab©github.com/jimjiminny / app 8<> Code87 Pull requests 31( Agents |© Actions•• Wiki © Security and quality 32 ~ Insights 3 Settings@ On April 24 we'll start using GitHub Copilot interaction data for Al model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.JY-20157 add not enough activities notification #12011 •$1 Open LakyLak wants to merge 2 commits into master from JY-20157-AJ-report-not-send-notification@) Conversation o• Commits 2|- Checks 21E Files changed 13A © All commits +Q Filter files...apo/Console/Commands/Reports/AutomatedReportsCommand.ohp@ -61,21 +61,29 @ public function handle(): intv = Console/Commands/Renorts|Snow = Carbon: : now();E AutomatedReportsCommand...v Jobs/AutomatedReportsE RequestGenerateAskJiminnyR...SendReportNotGeneratedMail...v @ Listeners/AutomatedReports/U….E TrackAutomatedReportGener...v # Mail/ReportsS1sMondav = Snow->1SMonday)S1sr1rstDayUtMonth = Snow->day === 1;ScurrentMonth = Snow->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', [+ ReportNotGenerated.ohp |"1SMonday' => S1SMonday,~ E Services/Kiosk/AutomatedRepo…..AskJiminnyReportActivityServ…'isFirstDay0fMonth' => SisFirstDay0fMonth,'currentMonth' => ScurrentMonth.E AutomatedReportsService.php'isQuarterlyMonth' => SisQuarterlyMonth,~E resources/views/emails/reportsreport-not-generated.blade.php/I Process dailv revortsl• F tests/UnitSthis->processReports(AutomatedReportsService::FREQUENCYDAILY):~ Jobs/AutomatedReportsE ReguestGenerateAsk JiminnvR....v = listeners/AutomatedRenorts/U.₴ TrackAutomatedReportGener..v E Services/Kiosk/AutomatedRepo…..E AskJiminnyReportActivityServ....AutomatedReportsServiceActi…./ Process weekly renorts on Mondavcif (SisMondav) {64 +67 +74 +86 +@40@ Daily - Platform - nowQ Type to search100% C4 8• Fri 24 Apr 9:46:13• Checks pending Code • (Preview) -+384 -52 9000C 0 I 13 viewedSubmit review+10 -2 mane [ Viewed0 ...Snow = Carhon:.nowdSisMondav = Snow->1SMonday)"Sisweekend = $now->isWeekend():SisFirstDay0fMonth = Snow->day === 1;ScurrentMonth = Snow->month.SisManualTrigger = $this->option('report-id') !== null;// Check if the current month is a quarterly month (January, April, July, October)$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);Sthis->loager->info(self::L0G PREFIX . ' Checkina conditions'. [I"isMonday' => SisMonday,'isweekend' => $isWeekend,'isFirstDay0fMonth' => $isFirstDay0fMonth,'currentMonth' => ScurrentMonth.l'isQuarterlyMonth' => SisQuarterlyMonth,/ Process dailv renorts on weekdavs onlv (skio Saturdav/Sundav)...// Manual triggers via --report-id bypass the weekend skip.if (I Sisweekend || SisManualTriager) {Sthis->processReports(AutomatedReportsService::FREQUENCY_DAILY):} else {ISthis->logger->info(self::L0G PREFIX . ' Skipping daily reports on weekend'):/ Process weekly renorts on Mondavslif (SisMonday) {...
|
PhpStorm
|
|
NULL
|
76729
|
|
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
NULL
|
76730
|
|
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
NULL
|
76731
|
|
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
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Jiminny\Jobs\AutomatedReports\SendReportJob;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command as CommandAlias;
class AutomatedReportsSendCommand extends Command
{
private const string LOG_PREFIX = '[automated-reports:send]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports:send
{--result-id= : Force send a specific AutomatedReportResult by ID, bypassing the scheduled time check}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends automated reports based on user timezone';
public function __construct(
private readonly LoggerInterface $logger,
private readonly AutomatedReportsRepository $reportRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly BusDispatcher $dispatcher,
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$resultId = $this->option('result-id');
if ($resultId !== null) {
return $this->handleForceSend((int) $resultId);
}
$reportResults = $this->reportRepository->getGeneratedNotSentResults();
foreach ($reportResults as $reportResult) {
/** @var AutomatedReportResult $reportResult */
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if ($this->automatedReportsService->shouldSendReport($validRecipients, $reportResult->getGeneratedAt())) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching job', [
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
}
}
return CommandAlias::SUCCESS;
}
private function handleForceSend(int $resultId): int
{
$reportResult = AutomatedReportResult::find($resultId);
if ($reportResult === null) {
$this->logger->error(self::LOG_PREFIX . ' Result not found', ['result_id' => $resultId]);
return CommandAlias::FAILURE;
}
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if (empty($validRecipients)) {
$this->logger->error(self::LOG_PREFIX . ' No valid recipients found', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
return CommandAlias::FAILURE;
}
$this->logger->info(self::LOG_PREFIX . ' Force dispatching job', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
return CommandAlias::SUCCESS;
}
}
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsSendCommand.php
|
NULL
|
76732
|
|
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
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Jiminny\Jobs\AutomatedReports\SendReportJob;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command as CommandAlias;
class AutomatedReportsSendCommand extends Command
{
private const string LOG_PREFIX = '[automated-reports:send]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports:send
{--result-id= : Force send a specific AutomatedReportResult by ID, bypassing the scheduled time check}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends automated reports based on user timezone';
public function __construct(
private readonly LoggerInterface $logger,
private readonly AutomatedReportsRepository $reportRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly BusDispatcher $dispatcher,
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$resultId = $this->option('result-id');
if ($resultId !== null) {
return $this->handleForceSend((int) $resultId);
}
$reportResults = $this->reportRepository->getGeneratedNotSentResults();
foreach ($reportResults as $reportResult) {
/** @var AutomatedReportResult $reportResult */
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if ($this->automatedReportsService->shouldSendReport($validRecipients, $reportResult->getGeneratedAt())) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching job', [
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
}
}
return CommandAlias::SUCCESS;
}
private function handleForceSend(int $resultId): int
{
$reportResult = AutomatedReportResult::find($resultId);
if ($reportResult === null) {
$this->logger->error(self::LOG_PREFIX . ' Result not found', ['result_id' => $resultId]);
return CommandAlias::FAILURE;
}
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if (empty($validRecipients)) {
$this->logger->error(self::LOG_PREFIX . ' No valid recipients found', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
return CommandAlias::FAILURE;
}
$this->logger->info(self::LOG_PREFIX . ' Force dispatching job', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
return CommandAlias::SUCCESS;
}
}
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsSendCommand.php
|
NULL
|
76733
|
|
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
90
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
public const string FREQUENCY_DAILY = 'daily';
public const string FREQUENCY_WEEKLY = 'weekly';
public const string FREQUENCY_MONTHLY = 'monthly';
public const string FREQUENCY_QUARTERLY = 'quarterly';
public const string FREQUENCY_ONE_OFF = 'one_off';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false): array
{
$options = $this->getTeams();
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = []): array
{
$data = [];
$teams = $this->getTeams();
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$period = $this->calculateFromAndToDatePeriod($frequency);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
private function formatReportPeriodName(string $frequency, Carbon $from, Carbon $to): string
{
$fromYear = $from->format('Y');
$toYear = $to->format('Y');
$differentYears = $fromYear !== $toYear;
switch ($frequency) {
case self::FREQUENCY_DAILY:
return $from->format('j M Y');
case self::FREQUENCY_QUARTERLY:
// 'Jan-Mar 2025' or 'Nov 2024-Jan 2025' if years differ
$startMonth = $from->format('M');
$endMonth = $to->copy()->subMonth();
$endMonthName = $endMonth->format('M');
$endMonthYear = $endMonth->format('Y');
if ($differentYears) {
return "{$startMonth} {$fromYear} - {$endMonthName} {$endMonthYear}";
}
return "{$startMonth} - {$endMonthName} {$toYear}";
case self::FREQUENCY_MONTHLY:
// 'May 2025' - monthly reports are always within the same year
return $from->format('M Y');
case self::FREQUENCY_WEEKLY:
// '4 - 8 Aug 2025', '27 Oct - 3 Nov 2025', or '28 Dec 2024 - 3 Jan 2025' if years differ
$startDay = $from->format('j');
$endDay = $to->format('j');
$startMonth = $from->format('M');
$endMonth = $to->format('M');
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
if ($startMonth !== $endMonth) {
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
}
return "{$startDay} - {$endDay} {$endMonth} {$toYear}";
case self::FREQUENCY_ONE_OFF:
// '2 May-31 May 2025' or '15 Dec 2024-15 Jan 2025' if years differ
$startDay = $from->format('j');
$startMonth = $from->format('M');
$endDay = $to->format('j');
$endMonth = $to->format('M');
// If same month and year, use a format like '2-31 May 2025'
if ($startMonth === $endMonth && ! $differentYears) {
return "{$startDay} - {$endDay} {$startMonth} {$toYear}";
}
// If different years, include both years
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
// Same year but different months
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
default:
// Default format for unknown frequencies
return $from->format('j M Y') . ' - ' . $to->format('j M Y');
}
}
public function getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
],
[
'id' => 'frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'label' => 'Frequency',
'required' => true,
'placeholder' => 'Select',
'options' => self::ASK_JIMINNY_FREQUENCIES,
'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,
],
[
'id' => 'expires_on',
'inputType' => InputTypeEnum::D...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
NULL
|
76734
|
|
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
90
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
public const string FREQUENCY_DAILY = 'daily';
public const string FREQUENCY_WEEKLY = 'weekly';
public const string FREQUENCY_MONTHLY = 'monthly';
public const string FREQUENCY_QUARTERLY = 'quarterly';
public const string FREQUENCY_ONE_OFF = 'one_off';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false): array
{
$options = $this->getTeams();
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = []): array
{
$data = [];
$teams = $this->getTeams();
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$period = $this->calculateFromAndToDatePeriod($frequency);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
private function formatReportPeriodName(string $frequency, Carbon $from, Carbon $to): string
{
$fromYear = $from->format('Y');
$toYear = $to->format('Y');
$differentYears = $fromYear !== $toYear;
switch ($frequency) {
case self::FREQUENCY_DAILY:
return $from->format('j M Y');
case self::FREQUENCY_QUARTERLY:
// 'Jan-Mar 2025' or 'Nov 2024-Jan 2025' if years differ
$startMonth = $from->format('M');
$endMonth = $to->copy()->subMonth();
$endMonthName = $endMonth->format('M');
$endMonthYear = $endMonth->format('Y');
if ($differentYears) {
return "{$startMonth} {$fromYear} - {$endMonthName} {$endMonthYear}";
}
return "{$startMonth} - {$endMonthName} {$toYear}";
case self::FREQUENCY_MONTHLY:
// 'May 2025' - monthly reports are always within the same year
return $from->format('M Y');
case self::FREQUENCY_WEEKLY:
// '4 - 8 Aug 2025', '27 Oct - 3 Nov 2025', or '28 Dec 2024 - 3 Jan 2025' if years differ
$startDay = $from->format('j');
$endDay = $to->format('j');
$startMonth = $from->format('M');
$endMonth = $to->format('M');
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
if ($startMonth !== $endMonth) {
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
}
return "{$startDay} - {$endDay} {$endMonth} {$toYear}";
case self::FREQUENCY_ONE_OFF:
// '2 May-31 May 2025' or '15 Dec 2024-15 Jan 2025' if years differ
$startDay = $from->format('j');
$startMonth = $from->format('M');
$endDay = $to->format('j');
$endMonth = $to->format('M');
// If same month and year, use a format like '2-31 May 2025'
if ($startMonth === $endMonth && ! $differentYears) {
return "{$startDay} - {$endDay} {$startMonth} {$toYear}";
}
// If different years, include both years
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
// Same year but different months
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
default:
// Default format for unknown frequencies
return $from->format('j M Y') . ' - ' . $to->format('j M Y');
}
}
public function getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
],
[
'id' => 'frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'label' => 'Frequency',
'required' => true,
'placeholder' => 'Select',
'options' => self::ASK_JIMINNY_FREQUENCIES,
'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,
],
[
'id' => 'expires_on',
'inputType' => InputTypeEnum::D...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
NULL
|
76735
|
|
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
102
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
public const string FREQUENCY_DAILY = 'daily';
public const string FREQUENCY_WEEKLY = 'weekly';
public const string FREQUENCY_MONTHLY = 'monthly';
public const string FREQUENCY_QUARTERLY = 'quarterly';
public const string FREQUENCY_ONE_OFF = 'one_off';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false): array
{
$options = $this->getTeams();
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = []): array
{
$data = [];
$teams = $this->getTeams();
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$period = $this->calculateFromAndToDatePeriod($frequency);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
private function formatReportPeriodName(string $frequency, Carbon $from, Carbon $to): string
{
$fromYear = $from->format('Y');
$toYear = $to->format('Y');
$differentYears = $fromYear !== $toYear;
switch ($frequency) {
case self::FREQUENCY_DAILY:
return $from->format('j M Y');
case self::FREQUENCY_QUARTERLY:
// 'Jan-Mar 2025' or 'Nov 2024-Jan 2025' if years differ
$startMonth = $from->format('M');
$endMonth = $to->copy()->subMonth();
$endMonthName = $endMonth->format('M');
$endMonthYear = $endMonth->format('Y');
if ($differentYears) {
return "{$startMonth} {$fromYear} - {$endMonthName} {$endMonthYear}";
}
return "{$startMonth} - {$endMonthName} {$toYear}";
case self::FREQUENCY_MONTHLY:
// 'May 2025' - monthly reports are always within the same year
return $from->format('M Y');
case self::FREQUENCY_WEEKLY:
// '4 - 8 Aug 2025', '27 Oct - 3 Nov 2025', or '28 Dec 2024 - 3 Jan 2025' if years differ
$startDay = $from->format('j');
$endDay = $to->format('j');
$startMonth = $from->format('M');
$endMonth = $to->format('M');
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
if ($startMonth !== $endMonth) {
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
}
return "{$startDay} - {$endDay} {$endMonth} {$toYear}";
case self::FREQUENCY_ONE_OFF:
// '2 May-31 May 2025' or '15 Dec 2024-15 Jan 2025' if years differ
$startDay = $from->format('j');
$startMonth = $from->format('M');
$endDay = $to->format('j');
$endMonth = $to->format('M');
// If same month and year, use a format like '2-31 May 2025'
if ($startMonth === $endMonth && ! $differentYears) {
return "{$startDay} - {$endDay} {$startMonth} {$toYear}";
}
// If different years, include both years
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
// Same year but different months
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
default:
// Default format for unknown frequencies
return $from->format('j M Y') . ' - ' . $to->format('j M Y');
}
}
public function getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
],
[
'id' => 'frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'label' => 'Frequency',
'required' => true,
'placeholder' => 'Select',
'options' => self::ASK_JIMINNY_FREQUENCIES,
'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,
],
[
'id' => 'expires_on',
'inputType' => InputTypeE...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
NULL
|
76736
|
|
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
102
3
34
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Kiosk\AutomatedReports;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\Exceptions\InvalidFormatException;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Jiminny\Component\ActivitySearch\FilterDefinition\InputTypeEnum;
use Jiminny\Component\AskAnything\AskAnythingPromptService;
use Jiminny\Component\AskAnything\Dtos\AskAnythingPromptDto;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Contracts\Repositories\PlaybookCategoryRepository;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Repositories\UserRepository;
use Jiminny\Exceptions\ApplicationException;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\ModelNotFoundException;
use Jiminny\Jobs\AutomatedReports\RequestGenerateReportJob;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\AskAnything\AskAnythingPrompt;
use Jiminny\Models\AskAnything\AskAnythingPromptTarget;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Contracts\UserContract;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\AskAnythingRepository;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Repositories\GroupRepository;
use Jiminny\Repositories\SearchRepository;
use Jiminny\Repositories\StageRepository;
use Throwable;
class AutomatedReportsService
{
public const string TYPE_LOSS_ANALYSIS = 'loss_analysis';
public const string TYPE_ASK_JIMINNY = 'ask_jiminny';
/**
* Standard report types (used by kiosk for existing automated reports).
*/
// @TODO this will add filter, however if we need to control feature by FF we need conditional logic
public const array TYPES = [
['id' => 'exec_summary', 'name' => 'Exec Summary'],
['id' => 'coaching_profiles', 'name' => 'Coaching Profiles'],
['id' => 'product_feedback', 'name' => 'Product Feedback'],
['id' => self::TYPE_LOSS_ANALYSIS, 'name' => 'Loss Analysis'],
// ['id' => 'questions', 'name' => 'Questions'],
// ['id' => 'statistical_quant', 'name' => 'Statistical Quantitative'],
];
public const array ALL_TYPES = [
...self::TYPES,
['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'],
];
public const string FREQUENCY_DAILY = 'daily';
public const string FREQUENCY_WEEKLY = 'weekly';
public const string FREQUENCY_MONTHLY = 'monthly';
public const string FREQUENCY_QUARTERLY = 'quarterly';
public const string FREQUENCY_ONE_OFF = 'one_off';
/**
* Frequencies for standard (non-Ask Jiminny) reports.
*/
public const array FREQUENCIES = [
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
/**
* Frequencies for Ask Jiminny reports.
*/
public const array ASK_JIMINNY_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
];
public const string MEDIA_TYPE_PDF = 'pdf';
public const string MEDIA_TYPE_PODCAST = 'podcast';
public const array MEDIA_TYPES = [self::MEDIA_TYPE_PDF, self::MEDIA_TYPE_PODCAST];
public const array MEDIA_TYPE_OBJECT_PDF = ['id' => self::MEDIA_TYPE_PDF, 'name' => 'PDF'];
public const array MEDIA_TYPE_OBJECT_PODCAST = ['id' => self::MEDIA_TYPE_PODCAST, 'name' => 'Podcast'];
public const array MEDIA_TYPE_OBJECTS = [self::MEDIA_TYPE_OBJECT_PDF, self::MEDIA_TYPE_OBJECT_PODCAST];
public const array CALL_TYPE_CONFERENCE = ['id' => 'conference', 'name' => 'Conference'];
public const array CALL_TYPE_DIALER = ['id' => 'dialer', 'name' => 'Dialer'];
public const int SENT_REPORT_AT_HOURS = 5;
public const string PDF_KEY = 'pdf';
public const string AUDIO_KEY = 'audio';
private const array ALL_FREQUENCIES = [
['id' => self::FREQUENCY_DAILY, 'name' => 'Daily'],
['id' => self::FREQUENCY_WEEKLY, 'name' => 'Weekly'],
['id' => self::FREQUENCY_MONTHLY, 'name' => 'Monthly'],
['id' => self::FREQUENCY_QUARTERLY, 'name' => 'Quarterly'],
['id' => self::FREQUENCY_ONE_OFF, 'name' => 'One-off'],
];
private const string S3_DIR = 'reports';
private const array FILE_EXTENSIONS_VARIANTS = ['html', 'MD', 'pdf'];
private const array FILE_PODCAST_EXTENSIONS_VARIANTS = ['json', 'mp3', 'ssml'];
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly GroupRepository $groupRepository,
private readonly UserRepository $userRepository,
private readonly StageRepository $stageRepository,
private readonly DealStagesService $dealStagesService,
private readonly RecipientsService $recipientsService,
private readonly AutomatedReportsRepository $automatedReportsRepository,
private readonly Webhook $webhookService,
private readonly BusDispatcher $dispatcher,
private readonly ActivityTypeService $activityTypeService,
private readonly PlaybookCategoryRepository $playbookCategoryRepository,
private readonly AskAnythingPromptService $askAnythingPromptService,
private readonly SearchRepository $activitySearchRepository,
private readonly AskAnythingRepository $askAnythingRepository,
) {
}
public static function getTypes(): array
{
$types = self::TYPES;
return array_map(static function ($type) {
return $type['id'];
}, $types);
}
public static function getCallTypes(): array
{
return array_map(static function ($callType) {
return $callType['id'];
}, [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER]);
}
public static function getFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::FREQUENCIES);
}
// front-facing structure
public function getReportEnabledFieldData(bool $value = false): array
{
return [
'id' => 'report_enabled',
'label' => '',
'inputType' => InputTypeEnum::TOGGLE,
'value' => $value,
];
}
// Organizations = Teams
public function getOrganizationFieldData(?string $value = null, bool $shortVersion = false): array
{
$options = $this->getTeams();
if ($shortVersion) {
return [
'id' => 'organization',
'label' => 'Organization',
'options' => $options,
];
}
return [
'id' => 'organization',
'label' => 'Organization',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $options,
'value' => $value,
'dependencies' => [
'teams',
'deal_stage_at_call',
'current_deal_stage',
'recipients',
ActivityTypeService::PLAYBOOK_CATEGORIES_KEY,
],
'dependsOn' => [],
];
}
// Teams = Groups
public function getTeamFieldData(array $options = [], array $value = [], bool $shortVersion = false): array
{
if ($shortVersion) {
return [
'id' => 'teams',
'label' => 'Team',
'options' => $options,
];
}
return [
'id' => 'teams',
'label' => 'Team',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => false,
'placeholder' => 'Select',
'options' => $options,
'value' => $value, // value should be an array of objects {id, name}
'dependencies' => [ActivityTypeService::PLAYBOOK_CATEGORIES_KEY],
'dependsOn' => [],
];
}
public function getReportTypeFieldData(?string $value = null, bool $shortVersion = false, ?Team $team = null): array
{
$types = [];
if ($team instanceof Team) {
if ($team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
$types = self::TYPES;
}
if ($team->hasFeature(FeatureEnum::ASK_JIMINNY_REPORTS)) {
$types[] = ['id' => self::TYPE_ASK_JIMINNY, 'name' => 'Ask Jiminny'];
}
} else {
$types = self::TYPES;
}
if ($shortVersion) {
return [
'id' => 'report_type',
'label' => 'Report Type',
'options' => $types,
];
}
return [
'id' => 'report_type',
'label' => 'Report Type',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => $types,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getFrequencyFieldData(?string $value = null): array
{
return [
'id' => 'frequency',
'label' => 'Frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'required' => true,
'placeholder' => 'Select',
'options' => self::FREQUENCIES,
'value' => $value,
'dependencies' => ['period'],
'dependsOn' => [],
];
}
public function getPeriodFieldData(?string $valueStartDate = null, ?string $valueEndDate = null): array
{
return [
'id' => 'period',
'label' => 'Select one-off period',
'inputType' => InputTypeEnum::DATE_RANGE,
'required' => true,
'placeholder' => 'Select',
'value' => ['startDate' => $valueStartDate, 'endDate' => $valueEndDate],
'queryParams' => [
'startDate' => 'start_date_period',
'endDate' => 'end_date_period',
],
'dependencies' => [],
'dependsOn' => ['frequency'],
];
}
public function getActivityTypesFieldData(?Team $team = null, array $value = [], array $teamsFilter = []): array
{
return $this->activityTypeService->getActivityTypeFieldData(team: $team, value: $value, groupIds: $teamsFilter);
}
public function getDealStageAtCallFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getDealStageAtCallFieldData(team: $team, value: $value);
}
public function getCurrentDealStageFieldData(?Team $team = null, array $value = []): array
{
return $this->dealStagesService->getCurrentDealStageFieldData(team: $team, value: $value);
}
public function getDealValueFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'deal_value',
'label' => 'Deal Value',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_deal_value',
'max' => 'max_deal_value',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallTypeFieldData(bool $conferenceOn = false, bool $dialerOn = false): array
{
$value = [];
$conferenceOn && $value[] = self::CALL_TYPE_CONFERENCE;
$dialerOn && $value[] = self::CALL_TYPE_DIALER;
return [
'id' => 'call_type',
'label' => 'Call Type',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => [
self::CALL_TYPE_CONFERENCE,
self::CALL_TYPE_DIALER,
],
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getMediaTypeFieldData(?AutomatedReport $report = null): array
{
$value = [];
if ($report) {
$value = $this->transformMediaTypes($report);
}
return [
'id' => 'media_types',
'label' => 'Export as',
'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,
'required' => true,
'options' => self::MEDIA_TYPE_OBJECTS,
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCallDurationFieldData(?int $valueMin = null, ?int $valueMax = null): array
{
return [
'id' => 'call_duration',
'label' => 'Call Duration',
'inputType' => InputTypeEnum::INTEGER_RANGE,
'required' => false,
'value' => ['min' => $valueMin, 'max' => $valueMax],
'queryParams' => [
'min' => 'min_call_duration',
'max' => 'max_call_duration',
],
'dependencies' => [],
'dependsOn' => [],
];
}
public function getRecipientsFieldData(?Team $team = null, array $value = []): array
{
return $this->recipientsService->getRecipientsFieldData(team: $team, value: $value);
}
public function getJiminnyRecipientsFieldData(array $value = []): array
{
return $this->recipientsService->getJiminnyRecipientsFieldData($value);
}
public function getAdditionalPromptInputFieldData(?string $value = null): array
{
return [
'id' => 'additional_prompt_input',
'label' => 'Special requirements',
'inputType' => InputTypeEnum::TEXTAREA,
'required' => false,
'placeholder' => 'What should be the focus of the report?',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
public function getCustomReportNameFieldData(?string $value = null): array
{
return [
'id' => 'custom_name',
'label' => 'Custom report name',
'inputType' => InputTypeEnum::TEXT,
'required' => false,
'placeholder' => 'Enter custom name',
'value' => $value,
'dependencies' => [],
'dependsOn' => [],
];
}
// data providers
public function getTeams(): array
{
$teams = $this->teamRepository->getTeamsForKiosk(status: Team::STATUS_ACTIVE);
$teamData = [];
foreach ($teams as $team) {
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
continue;
}
$teamData[] = $this->transformTeam($team);
}
return $teamData;
}
public function getTeamGroups(string $teamUuid): array
{
$data = [];
$team = $this->getTeam($teamUuid);
if ($team !== null) {
$groups = $team->groups()->get();
foreach ($groups as $group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
];
}
}
return $data;
}
public function getTeamsGroupsOptions(array $filterTeamUuids = []): array
{
$data = [];
$teams = $this->getTeams();
foreach ($teams as $team) {
if (! empty($filterTeamUuids) && ! in_array($team['id'], $filterTeamUuids, true)) {
continue;
}
$data[] = [
'label' => $team['name'],
'groups' => $this->getTeamGroups($team['id']),
];
}
return $data;
}
public function getTeam(string $teamUuid): ?Team
{
return $this->teamRepository->idOrUuid($teamUuid);
}
public function getTeamById(int $teamId): ?Team
{
return $this->teamRepository->find($teamId);
}
public function getGroupsUuids(AutomatedReport $report): array
{
$uuids = [];
$reportGroups = $report->getGroups();
foreach ($reportGroups as $groupId) {
if ($group = $this->groupRepository->find($groupId)) {
$uuids[] = $group->getUuid();
}
}
return $uuids;
}
public function getPlaybookCategoriesUuids(AutomatedReport $report): array
{
$uuids = [];
$playbookCategories = $report->getPlaybookCategories();
foreach ($playbookCategories as $id) {
if ($category = $this->playbookCategoryRepository->find($id)) {
$uuids[] = $category->getUuid();
}
}
return $uuids;
}
public function getDealAtCallStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getDealAtCallStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getCurrentDealStagesUuids(AutomatedReport $report): array
{
$uuids = [];
$reportStages = $report->getCurrentDealStages();
foreach ($reportStages as $id) {
if ($stage = $this->stageRepository->find($id)) {
$uuids[] = $stage->getUuid();
}
}
return $uuids;
}
public function getUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getRecipients());
}
public function getJiminnyUsersUuids(AutomatedReport $report): array
{
return $this->extractUserUuids($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function extractUserUuids(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => $user->getUuid())
->values()
->all();
}
// get mail data
public function getRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getRecipients());
}
/**
* @return array<UserContract>
*/
public function getRecipientUserObjects(AutomatedReport $report): array
{
$userIds = $report->getRecipients()['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->values()
->all();
}
private function getJiminnyRecipientUsers(AutomatedReport $report): array
{
return $this->buildRecipientUsers($report->getJiminnyRecipients());
}
/**
* @param array<string, mixed> $recipients
*/
private function buildRecipientUsers(array $recipients): array
{
$userIds = $recipients['users'] ?? [];
return collect($userIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (UserContract $user) => [
'email' => $user->getEmailAddress(),
'name' => $user->getName(),
'timezone' => $user->getTimezone()->getName(),
])
->values()
->all();
}
public function getValidRecipientUsers(AutomatedReport $report, bool $includeJiminny = false): array
{
if ($report->isAskJiminnyReport()) {
$recipients = $this->resolveAskJiminnyRecipients($report);
} else {
$recipients = $this->getRecipientUsers($report);
if ($includeJiminny) {
$recipients = array_merge($recipients, $this->getJiminnyRecipientUsers($report));
}
}
$emails = [];
return array_values(array_filter(
$recipients,
static function ($recipient) use (&$emails) {
if (empty($recipient['email']) || in_array($recipient['email'], $emails, true)) {
return false;
}
$emails[] = $recipient['email'];
return true;
}
));
}
private function resolveAskJiminnyRecipients(AutomatedReport $report): array
{
$recipients = [];
$creator = $report->getCreator();
if ($creator !== null) {
$recipients[] = [
'email' => $creator->getEmailAddress(),
'name' => $creator->getName(),
'timezone' => $creator->getTimezone()->getName(),
];
}
return array_merge(
$recipients,
$this->buildRecipientUsers($report->getRecipients()),
$this->getGroupRecipientUsers($report),
);
}
private function getGroupRecipientUsers(AutomatedReport $report): array
{
$users = [];
foreach ($report->getGroups() as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group === null) {
continue;
}
foreach ($group->getMembers() as $member) {
$users[] = [
'email' => $member->getEmailAddress(),
'name' => $member->getName(),
'timezone' => $member->getTimezone()->getName(),
];
}
}
return $users;
}
public function getReportTypeName(AutomatedReportResult $report): string
{
$type = $report->getReport()->getType();
$getType = $this->transformReportType($type);
return $getType['name'];
}
public function getReportPeriodName(AutomatedReportResult $report): string
{
$from = $report->getFromDate();
$to = $report->getToDate();
$frequency = $report->getReport()->getFrequency();
if ($from === null || $to === null) {
if (! $report->getReport()->isAskJiminnyReport()) {
$invalidPeriod = $from === null ? 'from' : 'to';
throw new ApplicationException('Report period is invalid: ' . $invalidPeriod);
}
$period = $this->calculateFromAndToDatePeriod($frequency);
$from = $period['fromDate'];
$to = $period['toDate'];
}
return $this->formatReportPeriodName($frequency, $from, $to);
}
private function formatReportPeriodName(string $frequency, Carbon $from, Carbon $to): string
{
$fromYear = $from->format('Y');
$toYear = $to->format('Y');
$differentYears = $fromYear !== $toYear;
switch ($frequency) {
case self::FREQUENCY_DAILY:
return $from->format('j M Y');
case self::FREQUENCY_QUARTERLY:
// 'Jan-Mar 2025' or 'Nov 2024-Jan 2025' if years differ
$startMonth = $from->format('M');
$endMonth = $to->copy()->subMonth();
$endMonthName = $endMonth->format('M');
$endMonthYear = $endMonth->format('Y');
if ($differentYears) {
return "{$startMonth} {$fromYear} - {$endMonthName} {$endMonthYear}";
}
return "{$startMonth} - {$endMonthName} {$toYear}";
case self::FREQUENCY_MONTHLY:
// 'May 2025' - monthly reports are always within the same year
return $from->format('M Y');
case self::FREQUENCY_WEEKLY:
// '4 - 8 Aug 2025', '27 Oct - 3 Nov 2025', or '28 Dec 2024 - 3 Jan 2025' if years differ
$startDay = $from->format('j');
$endDay = $to->format('j');
$startMonth = $from->format('M');
$endMonth = $to->format('M');
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
if ($startMonth !== $endMonth) {
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
}
return "{$startDay} - {$endDay} {$endMonth} {$toYear}";
case self::FREQUENCY_ONE_OFF:
// '2 May-31 May 2025' or '15 Dec 2024-15 Jan 2025' if years differ
$startDay = $from->format('j');
$startMonth = $from->format('M');
$endDay = $to->format('j');
$endMonth = $to->format('M');
// If same month and year, use a format like '2-31 May 2025'
if ($startMonth === $endMonth && ! $differentYears) {
return "{$startDay} - {$endDay} {$startMonth} {$toYear}";
}
// If different years, include both years
if ($differentYears) {
return "{$startDay} {$startMonth} {$fromYear} - {$endDay} {$endMonth} {$toYear}";
}
// Same year but different months
return "{$startDay} {$startMonth} - {$endDay} {$endMonth} {$toYear}";
default:
// Default format for unknown frequencies
return $from->format('j M Y') . ' - ' . $to->format('j M Y');
}
}
public function getReportTeamsName(AutomatedReportResult $report): string
{
$groups = $report->getGroups();
if (empty($groups)) {
return 'All';
}
// Get group names from repository
$groupNames = [];
foreach ($groups as $groupId) {
$group = $this->groupRepository->find($groupId);
if ($group) {
$groupNames[] = $group->getName();
}
}
if (count($groupNames) === 1) {
// Single team format
$teamsName = $groupNames[0];
} else {
// Multiple teams format
$teamsName = implode(', ', $groupNames);
}
return $teamsName;
}
public function getReportFileName(AutomatedReportResult $report): string
{
$customName = $report->getReport()->getCustomName();
$periodName = $this->getReportPeriodName($report);
$filenameSuffix = $this->getFilenameSuffix($report);
if ($customName) {
if ($filenameSuffix) {
$customName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$customName} - {$periodName}");
}
$baseName = $this->getReportTypeName($report);
if ($filenameSuffix) {
$baseName .= " {$filenameSuffix}";
}
return $this->sanitizeFileName("{$baseName} - {$periodName} - {$this->getReportTeamsName($report)}");
}
public function getReportFileNameWithExtension(AutomatedReportResult $result): string
{
$extension = $this->getMediaTypeMetadata($result)['extension'];
return $this->getReportFileName($result) . '.' . $extension;
}
public function sanitizeFileName(string $fileName): string
{
return str_replace(['/', '\\'], '-', $fileName);
}
public function isUserRecipientOfReport(User $user, AutomatedReport $report): bool
{
$recipientIds = array_map('intval', $report->getRecipients()['users'] ?? []);
if (in_array($user->getId(), $recipientIds, true)) {
return true;
}
if ($report->isAskJiminnyReport()) {
$groupId = $user->getGroupId();
if ($groupId !== null && in_array($groupId, $report->getGroups(), true)) {
return true;
}
}
return false;
}
public function transformReportResults(Collection $automatedReportResults): array
{
$data = [];
foreach ($automatedReportResults as $automatedReportResult) {
/** @var AutomatedReportResult $automatedReportResult */
$report = $automatedReportResult->getReport();
$createdBy = $report->getCreator();
$creator = [
'id' => $createdBy?->getUuid(),
'name' => $createdBy?->getName(),
'email' => $createdBy?->getEmailAddress(),
'photoUrl' => $createdBy?->getPhotoUrl(),
];
$data[] = [
'id' => $automatedReportResult->getUuid(),
'name' => $automatedReportResult->getName(),
'frequency' => $this->transformFrequency($report->getFrequency()),
'recipients' => $this->buildRecipients($report),
'report_type' => $this->transformReportType($report->getType()),
'media_type' => $automatedReportResult->getMediaType(),
'downloadUrl' => $this->generateReportResultDownloadUrl($automatedReportResult),
'viewUrl' => $this->generateReportResultViewUrl($automatedReportResult),
'generated_at' => $automatedReportResult->getGeneratedAt()?->toIso8601String(),
'creator' => $creator,
];
}
return $data;
}
private function buildRecipients(AutomatedReport $report): array
{
$creatorUuid = $report->getCreator()?->getUuid();
$recipients = array_values(array_filter(
$this->transformRecipients($report->getRecipients()),
static fn (array $recipient): bool => $recipient['id'] !== $creatorUuid,
));
if (! $report->isAskJiminnyReport()) {
return $recipients;
}
return [
...array_values($this->transformGroups(team: $report->getTeam(), groupsIds: $report->getGroups())),
...$recipients,
];
}
public function hasCallTypeConference(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_CONFERENCE['id'], $report->getCallTypes(), true);
}
public function hasCallTypeDialer(AutomatedReport $report): bool
{
return in_array(self::CALL_TYPE_DIALER['id'], $report->getCallTypes(), true);
}
// transformers
private function transformTeam(Team $team): array
{
if (! $team->hasFeature(FeatureEnum::AUTOMATED_REPORTS)) {
return [];
}
return [
'id' => $team->getUuid(),
'name' => $team->getName(),
];
}
private function transformReportFullView(AutomatedReport $report): array
{
$base = $this->transformReportBase($report);
return $report->getType() === self::TYPE_ASK_JIMINNY
? $base + $this->transformAskJiminnyFields($report)
: $base + $this->transformStandardReportFields($report);
}
private function transformReportBase(AutomatedReport $report): array
{
return [
'id' => $report->getUuid(),
'organization' => $this->transformOrganization(team: $report->getTeam()),
'report_type' => $this->transformReportType($report->getType()),
'frequency' => $this->transformFrequency($report->getFrequency()),
];
}
private function transformStandardReportFields(AutomatedReport $report): array
{
$team = $report->getTeam();
return [
'report_enabled' => $report->getStatus(),
'start_date_period' => $report->getFrom()?->format('Y-m-d H:i:s'),
'end_date_period' => $report->getTo()?->format('Y-m-d H:i:s'),
'deal_value_min' => $report->getDealValueMin(),
'deal_value_max' => $report->getDealValueMax(),
'call_types' => $this->transformCallType($report->getCallTypes()),
'media_types' => $this->transformMediaTypes($report),
'call_duration_min' => $this->transformDurationToMinutes($report->getCallDurationMin()),
'call_duration_max' => $this->transformDurationToMinutes($report->getCallDurationMax()),
'teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'deal_at_call_stages' => $this->transformStages(team: $team, stagesIds: $report->getDealAtCallStages()),
'current_deal_stages' => $this->transformStages(team: $team, stagesIds: $report->getCurrentDealStages()),
'recipients' => $this->transformRecipients($report->getRecipients()),
'created_by' => $this->transformCreator($report->getCreator()),
'additional_prompt_input' => $report->getAdditionalPromptInput(),
'custom_name' => $report->getCustomName(),
'created_at' => $report->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $report->getUpdatedAt()->format('Y-m-d H:i:s'),
'deleted_at' => $report->getDeletedAt()?->format('Y-m-d H:i:s'),
];
}
private function transformAskJiminnyFields(AutomatedReport $report): array
{
$team = $report->getTeam();
$creatorId = $report->getAttribute('created_by');
$explicitUserIds = array_values(array_filter(
$report->getRecipients()['users'] ?? [],
static fn ($id) => $id !== $creatorId
));
return [
'report_name' => $report->getCustomName(),
'enabled' => $report->getStatus(),
'share_teams' => $this->transformGroups(team: $team, groupsIds: $report->getGroups()),
'share_users' => $this->transformRecipients(['users' => $explicitUserIds]),
'saved_search' => $this->transformSafeSearch($report->getSavedSearch()),
'ask_jiminny_prompt' => $this->transformAskJiminnyPrompt($report->getAskAnythingPrompt()),
'expires_on' => $report->getExpiresAt()?->format('Y-m-d'),
];
}
private function transformOrganization(?Team $team): array
{
return [
'id' => $team?->getUuid(),
'name' => $team?->getName(),
];
}
private function transformReportType(string $type): array
{
foreach (self::ALL_TYPES as $typeItem) {
if ($typeItem['id'] === $type) {
return $typeItem;
}
}
return [];
}
private function transformCallType(array $types): array
{
$result = [];
$callTypes = [self::CALL_TYPE_CONFERENCE, self::CALL_TYPE_DIALER];
foreach ($types as $type) {
foreach ($callTypes as $callTypeItem) {
if ($callTypeItem['id'] === $type) {
$result[] = $callTypeItem;
break;
}
}
}
return $result;
}
private function transformMediaTypes(AutomatedReport $report): array
{
$values = [];
foreach ($report->getMediaTypes() as $mediaType) {
if (! in_array($mediaType, self::MEDIA_TYPES, true)) {
continue;
}
$values[] = match ($mediaType) {
self::MEDIA_TYPE_PDF => self::MEDIA_TYPE_OBJECT_PDF,
self::MEDIA_TYPE_PODCAST => self::MEDIA_TYPE_OBJECT_PODCAST,
};
}
return $values;
}
private function transformFrequency(string $frequency): array
{
foreach (self::ALL_FREQUENCIES as $frequencyItem) {
if ($frequencyItem['id'] === $frequency) {
return $frequencyItem;
}
}
return [];
}
public function transformDurationToMinutes(?int $duration): ?int
{
if (! $duration) {
return null;
}
return (int) ($duration / 60);
}
private function transformGroups(?Team $team, array $groupsIds): array
{
if (empty($groupsIds) || ! $team) {
return [];
}
$data = [];
foreach ($groupsIds as $groupId) {
$group = $team->groups()->where('id', $groupId)->first();
if ($group) {
$data[] = [
'id' => $group->getUuid(),
'name' => $group->getName(),
'photoUrl' => $group->getPhotoUrl(),
];
}
}
return $data;
}
private function transformStages(?Team $team, array $stagesIds): array
{
if (empty($stagesIds) || ! $team) {
return [];
}
$data = [];
foreach ($stagesIds as $stageId) {
$stage = $team->stages()->where('id', $stageId)->first();
if ($stage) {
$data[] = [
'id' => $stage->getUuid(),
'name' => $stage->getName(),
];
}
}
return $data;
}
private function transformRecipients(array $recipients): array
{
$users = [];
foreach ($recipients['users'] ?? [] as $userId) {
$users[] = $this->transformUser($userId);
}
return $users;
}
private function transformCreator(?User $user): ?array
{
if ($user === null) {
return null;
}
return $this->transformUser($user->getId());
}
private function transformAskJiminnyPrompt(?AskAnythingPrompt $prompt): ?array
{
if ($prompt === null) {
return null;
}
return [
'id' => $prompt->getUuid(),
'name' => $prompt->getTitle(),
];
}
private function transformSafeSearch(?Search $search): ?array
{
if ($search === null) {
return null;
}
return [
'id' => $search->getUuid(),
'name' => $search->getName(),
];
}
private function transformUser(int $userId): array
{
/* @var ?User $user */
$user = $this->userRepository->find($userId);
return [
'id' => $user?->getUuid(),
'name' => $user?->getName(),
'email' => $user?->getEmailAddress(),
'photoUrl' => $user?->getPhotoUrl(),
];
}
public function create(array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$validatedData['created_by'] = auth()->id();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
public function update(string $uuid, array $data): array
{
$validatedData = $this->validateAndTransformData($data);
$report = $this->automatedReportsRepository->findByUuid($uuid);
if (! $report) {
throw new InvalidArgumentException('Report not found');
}
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
$this->generateOneOffReport($automatedReport);
return $this->transformReportFullView($automatedReport);
}
/**
* Create an Ask Jiminny report.
*/
public function createAskJiminnyReport(array $data, User $creator): array
{
$validatedData = $this->validateAskJiminnyReportData($data, $creator);
$validatedData['created_by'] = $creator->getId();
$automatedReport = $this->automatedReportsRepository->create($validatedData);
return $this->transformReportFullView($automatedReport);
}
/**
* Update an Ask Jiminny report.
*/
public function updateAskJiminnyReport(AutomatedReport $report, array $data, User $user): array
{
if (! $report->isAskJiminnyReport()) {
throw new InvalidArgumentException('Report is not an Ask Jiminny report');
}
$validatedData = $this->validateAskJiminnyReportData($data, $user);
$oldCustomName = $report->getCustomName();
$automatedReport = $this->automatedReportsRepository->update($report, $validatedData);
if ($oldCustomName !== $automatedReport->getCustomName()) {
$this->updateResultNames($automatedReport);
}
return $this->transformReportFullView($automatedReport);
}
public function updateAskJiminnyReportStatus(AutomatedReport $report, bool $status): array
{
$this->automatedReportsRepository->update($report, ['status' => $status]);
return $this->transformReportFullView($report->fresh());
}
/**
* Validate and transform data for Ask Jiminny reports.
*/
private function validateAskJiminnyReportData(array $data, User $user): array
{
// Validate name
$name = trim($data['report_name'] ?? '');
if (empty($name)) {
throw new InvalidArgumentException('Report name is required');
}
if (mb_strlen($name) > 50) {
throw new InvalidArgumentException('Report name must be 50 characters or less');
}
// Validate frequency (only daily, weekly, monthly for Ask Jiminny)
$frequency = $data['frequency'] ?? null;
$askJiminnyFrequencies = [self::FREQUENCY_DAILY, self::FREQUENCY_WEEKLY, self::FREQUENCY_MONTHLY];
if (! in_array($frequency, $askJiminnyFrequencies, true)) {
throw new InvalidArgumentException('Frequency must be daily, weekly, or monthly');
}
// Validate expiration date
$expiresAt = $data['expires_on'] ?? null;
if (empty($expiresAt)) {
throw new InvalidArgumentException('Expiration date is required');
}
try {
$expiresAtDate = Carbon::parse($expiresAt);
} catch (InvalidFormatException $e) {
throw new InvalidArgumentException('Expiration date format is invalid');
}
$maxExpiration = Carbon::now()->addYear()->endOfDay();
if ($expiresAtDate->gt($maxExpiration)) {
throw new InvalidArgumentException('Expiration date cannot be more than 1 year from now');
}
if ($expiresAtDate->isPast()) {
throw new InvalidArgumentException('Expiration date cannot be in the past');
}
// Validate saved search
$activitySearchId = $data['saved_search'] ?? null;
if (empty($activitySearchId)) {
throw new InvalidArgumentException('Saved search is required');
}
$savedSearch = $this->activitySearchRepository->findByUuidAndUser($activitySearchId, $user);
if (! $savedSearch) {
throw new InvalidArgumentException('Saved search not found or does not belong to you');
}
// Validate saved prompt
$askAnythingPromptId = $data['ask_jiminny_prompt'] ?? null;
if (empty($askAnythingPromptId)) {
throw new InvalidArgumentException('Ask Jiminny prompt is required');
}
$prompt = $this->askAnythingRepository->getPromptByUuid($askAnythingPromptId);
if (! $prompt) {
throw new InvalidArgumentException('Ask Jiminny prompt not found');
}
// Validate status
$status = $data['enabled'] ?? false;
$recipientUserIds = [$user->getId()];
if (! empty($data['share_users'])) {
$sharedUserIds = $this->validateAndGetUserIdsByTeam(
$user->team,
(array) $data['share_users']
);
$recipientUserIds = array_merge($recipientUserIds, $sharedUserIds);
}
$sharedGroupIds = [];
if (! empty($data['share_teams'])) {
$sharedGroupIds = $this->validateAndGetGroupIds($user->team, (array) $data['share_teams']);
}
$recipientUserIds = array_values(array_unique($recipientUserIds));
return [
'team_id' => $user->getTeamId(),
'type' => self::TYPE_ASK_JIMINNY,
'status' => (bool) $status,
'frequency' => $frequency,
'custom_name' => $name,
'activity_search_id' => $savedSearch->getId(),
'ask_anything_prompt_id' => $prompt->getId(),
'expires_at' => $expiresAtDate->toDateString(),
'media_types' => [self::MEDIA_TYPE_PDF],
'call_types' => [],
'recipients' => ['users' => $recipientUserIds],
'groups' => $sharedGroupIds,
];
}
public static function getAskJiminnyFrequencies(): array
{
return array_map(static function ($frequency) {
return $frequency['id'];
}, self::ASK_JIMINNY_FREQUENCIES);
}
public function getAskJiminnyReportFilters(User $user): array
{
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
return [
[
'id' => 'prompt',
'label' => 'Prompt',
'options' => $prompts,
],
[
'id' => 'saved_search',
'label' => 'Saved Search',
'options' => $savedSearches,
],
];
}
public function getAskJiminnyReportFormData(User $user, ?AutomatedReport $report = null): array
{
$team = $user->getTeam();
$userTimezone = $user->getTimezone();
$savedSearches = $this->activitySearchRepository->findByUserOrderedByName($user)
->map(fn (Search $search) => [
'id' => $search->getUuid(),
'name' => $search->getName(),
])
->values()->all();
$prompts = collect(
$this->askAnythingPromptService->get($user, AskAnythingPromptTarget::on_demand)
)->map(fn (AskAnythingPromptDto $prompt) => [
'id' => $prompt->id,
'name' => $prompt->title,
])->values()->all();
$teamGroups = $this->groupRepository->getAllByTeam($team)->map(fn ($group) => [
'id' => $group->getUuid(),
'name' => $group->getName(),
])->values()->all();
$shareUsers = $this->recipientsService->getRecipientsFieldData(team: $team)['options'] ?? [];
$sharedTeamsValue = [];
$sharedUsersValue = [];
if ($report) {
$sharedTeamsValue = $this->transformGroups($team, $report->getGroups());
$recipientUserIds = $report->getRecipients()['users'] ?? [];
$creatorId = $report->getAttribute('created_by');
$sharedUserIds = array_values(array_filter(
$recipientUserIds,
static fn ($id) => $id !== $creatorId
));
$sharedUsersValue = collect($sharedUserIds)
->map(fn ($id) => $this->userRepository->find((int) $id))
->filter()
->map(fn (User $u) => [
'id' => $u->getUuid(),
'name' => $u->getName(),
])
->values()
->all();
}
return [
'fields' => [
[
'id' => 'enabled',
'inputType' => InputTypeEnum::TOGGLE,
'label' => '',
'value' => $report?->getStatus() ?? false,
],
[
'id' => 'report_name',
'inputType' => InputTypeEnum::TEXT,
'label' => 'Name',
'placeholder' => 'Enter name',
'required' => true,
'validation' => ['maxLength' => 50],
'value' => $report?->getCustomName() ?? '',
],
[
'id' => 'frequency',
'inputType' => InputTypeEnum::DROPDOWN,
'label' => 'Frequency',
'required' => true,
'placeholder' => 'Select',
'options' => self::ASK_JIMINNY_FREQUENCIES,
'value' => $report ? $this->transformFrequency($report->getFrequency()) : null,
],
[
'id' => 'expires_on',
'inputType' => InputTypeE...
|
PhpStorm
|
faVsco.js – AutomatedReportsService.php
|
NULL
|
76737
|
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
ai-team
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stefka Stoyanova
Adelina Petrova
Vasil Vasilev
Stoyan Tomov
Galya Dimitrova
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to first unread message (⌘J)
Mark as read (esc)
Jump to date
Lukas Kovalik
Yesterday at 10:47:31 AM
10:47 AM
сега забелязах че стария count който се ползва в при контрол на достъп canAccessAiReport брои само ресултати който са пратени, не тези които са генерирани
Nikolay Yankov
Yesterday at 1:31:46 PM
1:31 PM
какво значи които са пратени?
Yesterday at 1:32:17 PM
1:32
по принцип условието беше - ако за user-a има генерирано и може да го гледа на ai-reports страницата
Lukas Kovalik
Yesterday at 1:35:32 PM
1:35 PM
да, говоря за нещо още от преди, count който се гледа дали има user право да гледа ai-reports страница , брои пратени не тези който са генерирани само. Реално ако се праща всичко на ред почти няма да се вижда разлика
(edited)
Jump to date
Aneliya Angelova
Today at 11:13:41 AM
11:13 AM
Лукаш вчера генерирах 3 седмични репорта на. Галя на prod us. Обаче не ги пуснах по емейл и тази сутрин ги е получила по мейл.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:15:05 AM
11:15 AM
седмични не са ли в понеделник само
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 11:15:19 AM
11:15 AM
да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
Today at 11:15:56 AM
11:15
аз ги генерирах вчера. Да не би сутрин да минава джоба за разпращане на мейли и да събира всички генерирани репорти със статус 3 и да ги разпраща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:08 AM
11:16
без да се съобразява далки е дневен или седмичен или месечен
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:16:37 AM
11:16 AM
а да, за пращане мисля че няма проверки
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:56 AM
11:16
чакай да видя, мисля че само при генериране
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 11:18:33 AM
11:18 AM
Иначе ако това е обяснението - нека така си остане.
React with white_check_mark
React with eyes
React with raised_hands
DMSActivityMorerireroxToolsHelpcalMistorbookmarksJiminny …..vXStarredi• jiminny-x-integrati..8 platform-inner-teamE) Channels# ai-chapter# ai-team# alerts# backend# c-learning-peoplei confusion-clinic# curiosity_labadeal-insichts-dev# engineering# frontend# general# infra-changes# jiminny-bg8 people-with-copilo...8 people-with-zoom-# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the Deople of iimi...ProtllesWindow& platform-inner-...& 10MessagesChannel OverviewMoreYesterdayjinnylaop Aor 22nd Added by GitHubNikolay Ivanov 3:24 PMнякой нещо да е настроивал по githubactions. Почна да прави къмити вместо менбез да съм му разрешевал?https:/github.com/lminnv/app/pull/1200//changes/a68f42f210859f838a4fdced451f750627besoioИли нещо аз не разбирам?0AA0e 20 replies Last reply 18...Nikolay Yankov 3:50PMreplied to a uhread: някои нешо ла е насто..лол. ами предлагам маи ла му заораним лапускам към всички ла вилятNikolav Yankov 9.38 AMЩе се забавя за дейлито. Започнете без Мен.Aneliva Angelova 9:43 AMIДобро утро, няма да успея да вляза влейлито. Пествам ньлжовете.Message & platform-inner-team+ Aa I..•) New TabAl reports promotion pages by nik• JY-9712 | Nuges to expire after on8 Jiminnyu Userpilot Logged-activityJY-20157 add not enough activ XPipelines - jiminny/app+ New Tab©github.com/jimjiminny / app 8<> Code87 Pull requests 31( Agents |© Actions•• Wiki © Security and quality 32 ~ Insights 3 Settings@ On April 24 we'll start using GitHub Copilot interaction data for Al model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.JY-20157 add not enough activities notification #12011 •$1 Open LakyLak wants to merge 2 commits into master from JY-20157-AJ-report-not-send-notification@) Conversation o• Commits 2|- Checks 21E Files changed 13A © All commits +Q Filter files...apo/Console/Commands/Reports/AutomatedReportsCommand.ohp@ -61,21 +61,29 @ public function handle(): intv = Console/Commands/Renorts|Snow = Carbon: : now();E AutomatedReportsCommand...v Jobs/AutomatedReportsE RequestGenerateAskJiminnyR...SendReportNotGeneratedMail...v @ Listeners/AutomatedReports/U….E TrackAutomatedReportGener...v # Mail/ReportsS1sMondav = Snow->1SMonday)S1sr1rstDayUtMonth = Snow->day === 1;ScurrentMonth = Snow->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', [+ ReportNotGenerated.ohp |"1SMonday' => S1SMonday,~ E Services/Kiosk/AutomatedRepo…..AskJiminnyReportActivityServ…'isFirstDay0fMonth' => SisFirstDay0fMonth,'currentMonth' => ScurrentMonth.E AutomatedReportsService.php'isQuarterlyMonth' => SisQuarterlyMonth,~E resources/views/emails/reportsreport-not-generated.blade.php/I Process dailv revortsl• F tests/UnitSthis->processReports(AutomatedReportsService::FREQUENCYDAILY):~ Jobs/AutomatedReportsE ReguestGenerateAsk JiminnvR....v = listeners/AutomatedRenorts/U.₴ TrackAutomatedReportGener..v E Services/Kiosk/AutomatedRepo…..E AskJiminnyReportActivityServ....AutomatedReportsServiceActi…./ Process weekly renorts on Mondavcif (SisMondav) {64 +67 +74 +86 +@40@ Daily - Platform - nowQ Type to search100% C4 8• Fri 24 Apr 9:46:13• Checks pending Code • (Preview) -+384 -52 9000C 0 I 13 viewedSubmit review+10 -2 mane [ Viewed0 ...Snow = Carhon:.nowdSisMondav = Snow->1SMonday)"Sisweekend = $now->isWeekend():SisFirstDay0fMonth = Snow->day === 1;ScurrentMonth = Snow->month.SisManualTrigger = $this->option('report-id') !== null;// Check if the current month is a quarterly month (January, April, July, October)$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);Sthis->loager->info(self::L0G PREFIX . ' Checkina conditions'. [I"isMonday' => SisMonday,'isweekend' => $isWeekend,'isFirstDay0fMonth' => $isFirstDay0fMonth,'currentMonth' => ScurrentMonth.l'isQuarterlyMonth' => SisQuarterlyMonth,/ Process dailv renorts on weekdavs onlv (skio Saturdav/Sundav)...// Manual triggers via --report-id bypass the weekend skip.if (I Sisweekend || SisManualTriager) {Sthis->processReports(AutomatedReportsService::FREQUENCY_DAILY):} else {ISthis->logger->info(self::L0G PREFIX . ' Skipping daily reports on weekend'):/ Process weekly renorts on Mondavslif (SisMonday) {...
|
Slack
|
! Aneliya Angelova, Nikolay Yankov, Steliyan Georg ! Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - 1 new item - Slack...
|
NULL
|
76738
|
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
ai-team
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stefka Stoyanova
Adelina Petrova
Vasil Vasilev
Stoyan Tomov
Galya Dimitrova
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to first unread message (⌘J)
Mark as read (esc)
Jump to date
Lukas Kovalik
Yesterday at 10:47:31 AM
10:47 AM
сега забелязах че стария count който се ползва в при контрол на достъп canAccessAiReport брои само ресултати който са пратени, не тези които са генерирани
Nikolay Yankov
Yesterday at 1:31:46 PM
1:31 PM
какво значи които са пратени?
Yesterday at 1:32:17 PM
1:32
по принцип условието беше - ако за user-a има генерирано и може да го гледа на ai-reports страницата
Lukas Kovalik
Yesterday at 1:35:32 PM
1:35 PM
да, говоря за нещо още от преди, count който се гледа дали има user право да гледа ai-reports страница , брои пратени не тези който са генерирани само. Реално ако се праща всичко на ред почти няма да се вижда разлика
(edited)
Jump to date
Aneliya Angelova
Today at 11:13:41 AM
11:13 AM
Лукаш вчера генерирах 3 седмични репорта на. Галя на prod us. Обаче не ги пуснах по емейл и тази сутрин ги е получила по мейл.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:15:05 AM
11:15 AM
седмични не са ли в понеделник само
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 11:15:19 AM
11:15 AM
да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
Today at 11:15:56 AM
11:15
аз ги генерирах вчера. Да не би сутрин да минава джоба за разпращане на мейли и да събира всички генерирани репорти със статус 3 и да ги разпраща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:08 AM
11:16
без да се съобразява далки е дневен или седмичен или месечен
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:16:37 AM
11:16 AM
а да, за пращане мисля че няма проверки
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:56 AM
11:16
чакай да видя, мисля че само при генериране
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 11:18:33 AM
11:18 AM
Иначе ако това е обяснението - нека така си остане.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Jump to date
Lukas Kovalik
Yesterday at 10:47:31 AM
10:47 AM
сега забелязах че стария count който се ползва в при контрол на достъп canAccessAiReport брои само ресултати който са пратени, не тези които са генерирани
Nikolay Yankov
Yesterday at 1:31:46 PM
1:31 PM
какво значи които са пратени?
Yesterday at 1:32:17 PM
1:32
по принцип условието беше - ако за user-a има генерирано и може да го гледа на ai-reports страницата
Lukas Kovalik
Yesterday at 1:35:32 PM
1:35 PM
да, говоря за нещо още от преди, count който се гледа дали има user право да гледа ai-reports страница , брои пратени не тези който са генерирани само. Реално ако се праща всичко на ред почти няма да се вижда разлика
(edited)
Jump to date
Aneliya Angelova
Today at 11:13:41 AM
11:13 AM
Лукаш вчера генерирах 3 седмични репорта на. Галя на prod us. Обаче не ги пуснах по емейл и тази сутрин ги е получила по мейл.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:15:05 AM
11:15 AM
седмични не са ли в понеделник само
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 11:15:19 AM
11:15 AM
да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:15:56 AM
Firefox• 0FileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com•Daily - Platform - now100% K78 • Fri 24 Apr 9:46:13|=Pop out this videoNikolay NikolovStefka StoyanovaGalya DimitrovaLukas Kovalik9:46 AM | Daily - Platform• 0:27...
|
Slack
|
! Aneliya Angelova, Nikolay Yankov, Steliyan Georg ! Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - 1 new item - Slack...
|
NULL
|
76739
|
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
ai-team
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stefka Stoyanova
Adelina Petrova
Vasil Vasilev
Stoyan Tomov
Galya Dimitrova
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 10:47:31 AM
10:47 AM
сега забелязах че стария count който се ползва в при контрол на достъп canAccessAiReport брои само ресултати който са пратени, не тези които са генерирани
Nikolay Yankov
Yesterday at 1:31:46 PM
1:31 PM
какво значи които са пратени?
Yesterday at 1:32:17 PM
1:32
по принцип условието беше - ако за user-a има генерирано и може да го гледа на ai-reports страницата
Lukas Kovalik
Yesterday at 1:35:32 PM
1:35 PM
да, говоря за нещо още от преди, count който се гледа дали има user право да гледа ai-reports страница , брои пратени не тези който са генерирани само. Реално ако се праща всичко на ред почти няма да се вижда разлика
(edited)
Jump to date
Aneliya Angelova
Today at 11:13:41 AM
11:13 AM
Лукаш вчера генерирах 3 седмични репорта на. Галя на prod us. Обаче не ги пуснах по емейл и тази сутрин ги е получила по мейл.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:15:05 AM
11:15 AM
седмични не са ли в понеделник само
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 11:15:19 AM
11:15 AM
да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:15:56 AM
11:15
аз ги генерирах вчера. Да не би сутрин да минава джоба за разпращане на мейли и да събира всички генерирани репорти със статус 3 и да ги разпраща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:08 AM
11:16
без да се съобразява далки е дневен или седмичен или месечен
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:16:37 AM
11:16 AM
а да, за пращане мисля че няма проверки
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:56 AM
11:16
чакай да видя, мисля че само при генериране
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
Aneliya Angelova
Today at 11:18:33 AM
11:18 AM
Иначе ако това е обяснението - нека така си остане.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Adelina Petrova, Direct Message, 1 of 7 suggestions
Aneliya Angelova is typing
Firefox• 0FileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com•Daily - Platform - now100% K78 • Fri 24 Apr 9:46:13|=Pop out this videoNikolay NikolovStefka StoyanovaGalya DimitrovaLukas Kovalik9:46 AM | Daily - Platform• 0:27...
|
Slack
|
Aneliya Angelova, Nikolay Yankov, Steliyan Georgie Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - 1 new item - Slack...
|
NULL
|
76740
|
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
ai-team
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stefka Stoyanova
Adelina Petrova
Vasil Vasilev
Stoyan Tomov
Galya Dimitrova
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 10:47:31 AM
10:47 AM
сега забелязах че стария count който се ползва в при контрол на достъп canAccessAiReport брои само ресултати който са пратени, не тези които са генерирани
Nikolay Yankov
Yesterday at 1:31:46 PM
1:31 PM
какво значи които са пратени?
Yesterday at 1:32:17 PM
1:32
по принцип условието беше - ако за user-a има генерирано и може да го гледа на ai-reports страницата
Lukas Kovalik
Yesterday at 1:35:32 PM
1:35 PM
да, говоря за нещо още от преди, count който се гледа дали има user право да гледа ai-reports страница , брои пратени не тези който са генерирани само. Реално ако се праща всичко на ред почти няма да се вижда разлика
(edited)
Jump to date
Aneliya Angelova
Today at 11:13:41 AM
11:13 AM
Лукаш вчера генерирах 3 седмични репорта на. Галя на prod us. Обаче не ги пуснах по емейл и тази сутрин ги е получила по мейл.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:15:05 AM
11:15 AM
седмични не са ли в понеделник само
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 11:15:19 AM
11:15 AM
да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:15:56 AM
11:15
аз ги генерирах вчера. Да не би сутрин да минава джоба за разпращане на мейли и да събира всички генерирани репорти със статус 3 и да ги разпраща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:08 AM
11:16
без да се съобразява далки е дневен или седмичен или месечен
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:16:37 AM
11:16 AM
а да, за пращане мисля че няма проверки
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:56 AM
11:16
чакай да видя, мисля че само при генериране
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
Aneliya Angelova
Today at 11:18:33 AM
11:18 AM
Иначе ако това е обяснението - нека така си остане.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Adelina Petrova, Direct Message, 1 of 7 suggestions
Aneliya Angelova is typing
DMSActivityMorerireroxToolsHelpcalMistorbookmarksJiminny …..vXStarredi• jiminny-x-integrati..8 platform-inner-teamE) Channels# ai-chapter# ai-team# alerts# backend# c-learning-peoplei confusion-clinic# curiosity_labadeal-insichts-dev# engineering# frontend# general# infra-changes# jiminny-bg8 people-with-copilo...8 people-with-zoom-# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the Deople of iimi...ProtllesWindow& platform-inner-...& 10MessagesChannel OverviewMoreYesterdayjinnylaop Aor 22nd Added by GitHubNikolay Ivanov 3:24 PMнякой нещо да е настроивал по githubactions. Почна да прави къмити вместо менбез да съм му разрешевал?https:/github.com/lminnv/app/pull/1200//changes/a68f42f210859f838a4fdced451f750627besoioИли нещо аз не разбирам?0AA0e 20 replies Last reply 18...Nikolay Yankov 3:50PMreplied to a uhread: някои нешо ла е насто..лол. ами предлагам маи ла му заораним лапускам към всички ла вилятNikolav Yankov 9.38 AMЩе се забавя за дейлито. Започнете без Мен.Aneliva Angelova 9:43 AMIДобро утро, няма да успея да вляза влейлито. Пествам ньлжовете.Message & platform-inner-team+ Aa I..•) New TabAl reports promotion pages by nik• JY-9712 | Nuges to expire after on8 Jiminnyu Userpilot Logged-activityJY-20157 add not enough activ XPipelines - jiminny/app+ New Tab©github.com/jimjiminny / app 8<> Code87 Pull requests 31( Agents |© Actions•• Wiki © Security and quality 32 ~ Insights 3 Settings@ On April 24 we'll start using GitHub Copilot interaction data for Al model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.JY-20157 add not enough activities notification #12011 •$1 Open LakyLak wants to merge 2 commits into master from JY-20157-AJ-report-not-send-notification@) Conversation o• Commits 2|- Checks 21E Files changed 13A © All commits +Q Filter files...apo/Console/Commands/Reports/AutomatedReportsCommand.ohp@ -61,21 +61,29 @ public function handle(): intv = Console/Commands/Renorts|Snow = Carbon: : now();E AutomatedReportsCommand...v Jobs/AutomatedReportsE RequestGenerateAskJiminnyR...SendReportNotGeneratedMail...v @ Listeners/AutomatedReports/U….E TrackAutomatedReportGener...v # Mail/ReportsS1sMondav = Snow->1SMonday)S1sr1rstDayUtMonth = Snow->day === 1;ScurrentMonth = Snow->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', [+ ReportNotGenerated.ohp |"1SMonday' => S1SMonday,~ E Services/Kiosk/AutomatedRepo…..AskJiminnyReportActivityServ…'isFirstDay0fMonth' => SisFirstDay0fMonth,'currentMonth' => ScurrentMonth.E AutomatedReportsService.php'isQuarterlyMonth' => SisQuarterlyMonth,~E resources/views/emails/reportsreport-not-generated.blade.php/I Process dailv revortsl• F tests/UnitSthis->processReports(AutomatedReportsService::FREQUENCYDAILY):~ Jobs/AutomatedReportsE ReguestGenerateAsk JiminnvR....v = listeners/AutomatedRenorts/U.₴ TrackAutomatedReportGener..v E Services/Kiosk/AutomatedRepo…..E AskJiminnyReportActivityServ....AutomatedReportsServiceActi…./ Process weekly renorts on Mondavcif (SisMondav) {64 +67 +74 +86 +@40@ Daily - Platform - nowQ Type to search100% C4 8• Fri 24 Apr 9:46:13• Checks pending Code • (Preview) -+384 -52 9000C 0 I 13 viewedSubmit review+10 -2 mane [ Viewed0 ...Snow = Carhon:.nowdSisMondav = Snow->1SMonday)"Sisweekend = $now->isWeekend():SisFirstDay0fMonth = Snow->day === 1;ScurrentMonth = Snow->month.SisManualTrigger = $this->option('report-id') !== null;// Check if the current month is a quarterly month (January, April, July, October)$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);Sthis->loager->info(self::L0G PREFIX . ' Checkina conditions'. [I"isMonday' => SisMonday,'isweekend' => $isWeekend,'isFirstDay0fMonth' => $isFirstDay0fMonth,'currentMonth' => ScurrentMonth.l'isQuarterlyMonth' => SisQuarterlyMonth,/ Process dailv renorts on weekdavs onlv (skio Saturdav/Sundav)...// Manual triggers via --report-id bypass the weekend skip.if (I Sisweekend || SisManualTriager) {Sthis->processReports(AutomatedReportsService::FREQUENCY_DAILY):} else {ISthis->logger->info(self::L0G PREFIX . ' Skipping daily reports on weekend'):/ Process weekly renorts on Mondavslif (SisMonday) {...
|
Slack
|
Aneliya Angelova, Nikolay Yankov, Steliyan Georgie Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - 1 new item - Slack...
|
NULL
|
76741
|
|
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
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Jiminny\Jobs\AutomatedReports\SendReportJob;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command as CommandAlias;
class AutomatedReportsSendCommand extends Command
{
private const string LOG_PREFIX = '[automated-reports:send]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports:send
{--result-id= : Force send a specific AutomatedReportResult by ID, bypassing the scheduled time check}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends automated reports based on user timezone';
public function __construct(
private readonly LoggerInterface $logger,
private readonly AutomatedReportsRepository $reportRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly BusDispatcher $dispatcher,
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$resultId = $this->option('result-id');
if ($resultId !== null) {
return $this->handleForceSend((int) $resultId);
}
$reportResults = $this->reportRepository->getGeneratedNotSentResults();
foreach ($reportResults as $reportResult) {
/** @var AutomatedReportResult $reportResult */
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if ($this->automatedReportsService->shouldSendReport($validRecipients, $reportResult->getGeneratedAt())) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching job', [
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
}
}
return CommandAlias::SUCCESS;
}
private function handleForceSend(int $resultId): int
{
$reportResult = AutomatedReportResult::find($resultId);
if ($reportResult === null) {
$this->logger->error(self::LOG_PREFIX . ' Result not found', ['result_id' => $resultId]);
return CommandAlias::FAILURE;
}
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if (empty($validRecipients)) {
$this->logger->error(self::LOG_PREFIX . ' No valid recipients found', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
return CommandAlias::FAILURE;
}
$this->logger->info(self::LOG_PREFIX . ' Force dispatching job', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
return CommandAlias::SUCCESS;
}
}...
|
PhpStorm
|
faVsco.js – AutomatedReportsSendCommand.php
|
NULL
|
76742
|
|
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
DMSActivityMorerireroxToolsHelpcalMistorbookmarksJiminny …..vXStarredi• jiminny-x-integrati..8 platform-inner-teamE) Channels# ai-chapter# ai-team# alerts# backend# c-learning-peoplei confusion-clinic# curiosity_labadeal-insichts-dev# engineering# frontend# general# infra-changes# jiminny-bg8 people-with-copilo...8 people-with-zoom-# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the Deople of iimi...ProtllesWindow& platform-inner-...& 10MessagesChannel OverviewMoreYesterdayjinnylaop Aor 22nd Added by GitHubNikolay Ivanov 3:24 PMнякой нещо да е настроивал по githubactions. Почна да прави къмити вместо менбез да съм му разрешевал?https:/github.com/lminnv/app/pull/1200//changes/a68f42f210859f838a4fdced451f750627besoioИли нещо аз не разбирам?0AA0e 20 replies Last reply 18...Nikolay Yankov 3:50PMreplied to a uhread: някои нешо ла е насто..лол. ами предлагам маи ла му заораним лапускам към всички ла вилятNikolav Yankov 9.38 AMЩе се забавя за дейлито. Започнете без Мен.Aneliva Angelova 9:43 AMIДобро утро, няма да успея да вляза влейлито. Пествам ньлжовете.Message & platform-inner-team+ Aa I..•) New TabAl reports promotion pages by nik• JY-9712 | Nuges to expire after on8 Jiminnyu Userpilot Logged-activityJY-20157 add not enough activ XPipelines - jiminny/app+ New Tab©github.com/jimjiminny / app 8<> Code87 Pull requests 31( Agents |© Actions•• Wiki © Security and quality 32 ~ Insights 3 Settings@ On April 24 we'll start using GitHub Copilot interaction data for Al model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.JY-20157 add not enough activities notification #12011 •$1 Open LakyLak wants to merge 2 commits into master from JY-20157-AJ-report-not-send-notification@) Conversation o• Commits 2|- Checks 21E Files changed 13A © All commits +Q Filter files...apo/Console/Commands/Reports/AutomatedReportsCommand.ohp@ -61,21 +61,29 @ public function handle(): intv = Console/Commands/Renorts|Snow = Carbon: : now();E AutomatedReportsCommand...v Jobs/AutomatedReportsE RequestGenerateAskJiminnyR...SendReportNotGeneratedMail...v @ Listeners/AutomatedReports/U….E TrackAutomatedReportGener...v # Mail/ReportsS1sMondav = Snow->1SMonday)S1sr1rstDayUtMonth = Snow->day === 1;ScurrentMonth = Snow->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', [+ ReportNotGenerated.ohp |"1SMonday' => S1SMonday,~ E Services/Kiosk/AutomatedRepo…..AskJiminnyReportActivityServ…'isFirstDay0fMonth' => SisFirstDay0fMonth,'currentMonth' => ScurrentMonth.E AutomatedReportsService.php'isQuarterlyMonth' => SisQuarterlyMonth,~E resources/views/emails/reportsreport-not-generated.blade.php/I Process dailv revortsl• F tests/UnitSthis->processReports(AutomatedReportsService::FREQUENCYDAILY):~ Jobs/AutomatedReportsE ReguestGenerateAsk JiminnvR....v = listeners/AutomatedRenorts/U.₴ TrackAutomatedReportGener..v E Services/Kiosk/AutomatedRepo…..E AskJiminnyReportActivityServ....AutomatedReportsServiceActi…./ Process weekly renorts on Mondavcif (SisMondav) {64 +67 +74 +86 +@40@ Daily - Platform - nowQ Type to search100% C4 8• Fri 24 Apr 9:46:13• Checks pending Code • (Preview) -+384 -52 9000C 0 I 13 viewedSubmit review+10 -2 mane [ Viewed0 ...Snow = Carhon:.nowdSisMondav = Snow->1SMonday)"Sisweekend = $now->isWeekend():SisFirstDay0fMonth = Snow->day === 1;ScurrentMonth = Snow->month.SisManualTrigger = $this->option('report-id') !== null;// Check if the current month is a quarterly month (January, April, July, October)$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);Sthis->loager->info(self::L0G PREFIX . ' Checkina conditions'. [I"isMonday' => SisMonday,'isweekend' => $isWeekend,'isFirstDay0fMonth' => $isFirstDay0fMonth,'currentMonth' => ScurrentMonth.l'isQuarterlyMonth' => SisQuarterlyMonth,/ Process dailv renorts on weekdavs onlv (skio Saturdav/Sundav)...// Manual triggers via --report-id bypass the weekend skip.if (I Sisweekend || SisManualTriager) {Sthis->processReports(AutomatedReportsService::FREQUENCY_DAILY):} else {ISthis->logger->info(self::L0G PREFIX . ' Skipping daily reports on weekend'):/ Process weekly renorts on Mondavslif (SisMonday) {...
|
PhpStorm
|
faVsco.js – AutomatedReportsSendCommand.php
|
NULL
|
76743
|
|
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
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Jiminny\Jobs\AutomatedReports\SendReportJob;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command as CommandAlias;
class AutomatedReportsSendCommand extends Command
{
private const string LOG_PREFIX = '[automated-reports:send]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports:send
{--result-id= : Force send a specific AutomatedReportResult by ID, bypassing the scheduled time check}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends automated reports based on user timezone';
public function __construct(
private readonly LoggerInterface $logger,
private readonly AutomatedReportsRepository $reportRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly BusDispatcher $dispatcher,
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$resultId = $this->option('result-id');
if ($resultId !== null) {
return $this->handleForceSend((int) $resultId);
}
$reportResults = $this->reportRepository->getGeneratedNotSentResults();
foreach ($reportResults as $reportResult) {
/** @var AutomatedReportResult $reportResult */
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if ($this->automatedReportsService->shouldSendReport($validRecipients, $reportResult->getGeneratedAt())) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching job', [
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
}
}
return CommandAlias::SUCCESS;
}
private function handleForceSend(int $resultId): int
{
$reportResult = AutomatedReportResult::find($resultId);
if ($reportResult === null) {
$this->logger->error(self::LOG_PREFIX . ' Result not found', ['result_id' => $resultId]);
return CommandAlias::FAILURE;
}
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if (empty($validRecipients)) {
$this->logger->error(self::LOG_PREFIX . ' No valid recipients found', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
return CommandAlias::FAILURE;
}
$this->logger->info(self::LOG_PREFIX . ' Force dispatching job', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
return CommandAlias::SUCCESS;
}
}
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…...
|
PhpStorm
|
faVsco.js – AutomatedReportsSendCommand.php
|
NULL
|
76744
|
|
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
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Reports;
use Illuminate\Console\Command;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
use Jiminny\Jobs\AutomatedReports\SendReportJob;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Repositories\AutomatedReportsRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command as CommandAlias;
class AutomatedReportsSendCommand extends Command
{
private const string LOG_PREFIX = '[automated-reports:send]';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'automated-reports:send
{--result-id= : Force send a specific AutomatedReportResult by ID, bypassing the scheduled time check}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends automated reports based on user timezone';
public function __construct(
private readonly LoggerInterface $logger,
private readonly AutomatedReportsRepository $reportRepository,
private readonly AutomatedReportsService $automatedReportsService,
private readonly BusDispatcher $dispatcher,
) {
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$resultId = $this->option('result-id');
if ($resultId !== null) {
return $this->handleForceSend((int) $resultId);
}
$reportResults = $this->reportRepository->getGeneratedNotSentResults();
foreach ($reportResults as $reportResult) {
/** @var AutomatedReportResult $reportResult */
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if ($this->automatedReportsService->shouldSendReport($validRecipients, $reportResult->getGeneratedAt())) {
$this->logger->info(self::LOG_PREFIX . ' Dispatching job', [
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
}
}
return CommandAlias::SUCCESS;
}
private function handleForceSend(int $resultId): int
{
$reportResult = AutomatedReportResult::find($resultId);
if ($reportResult === null) {
$this->logger->error(self::LOG_PREFIX . ' Result not found', ['result_id' => $resultId]);
return CommandAlias::FAILURE;
}
$validRecipients = $this->automatedReportsService->getValidRecipientUsers($reportResult->getReport());
if (empty($validRecipients)) {
$this->logger->error(self::LOG_PREFIX . ' No valid recipients found', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
return CommandAlias::FAILURE;
}
$this->logger->info(self::LOG_PREFIX . ' Force dispatching job', [
'result_id' => $resultId,
'uuid' => $reportResult->getUuid(),
]);
$this->dispatcher->dispatch(new SendReportJob($reportResult->getUuid()));
return CommandAlias::SUCCESS;
}
}
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...
|
PhpStorm
|
faVsco.js – AutomatedReportsSendCommand.php
|
NULL
|
76745
|
|
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
DMSActivityMorerireroxToolsHelpcalMistorbookmarksJiminny …..vXStarredi• jiminny-x-integrati..8 platform-inner-teamE) Channels# ai-chapter# ai-team# alerts# backend# c-learning-peoplei confusion-clinic# curiosity_labadeal-insichts-dev# engineering# frontend# general# infra-changes# jiminny-bg8 people-with-copilo...8 people-with-zoom-# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the Deople of iimi...ProtllesWindow& platform-inner-...& 10MessagesChannel OverviewMoreYesterdayjinnylaop Aor 22nd Added by GitHubNikolay Ivanov 3:24 PMнякой нещо да е настроивал по githubactions. Почна да прави къмити вместо менбез да съм му разрешевал?https:/github.com/lminnv/app/pull/1200//changes/a68f42f210859f838a4fdced451f750627besoioИли нещо аз не разбирам?0AA0e 20 replies Last reply 18...Nikolay Yankov 3:50PMreplied to a uhread: някои нешо ла е насто..лол. ами предлагам маи ла му заораним лапускам към всички ла вилятNikolav Yankov 9.38 AMЩе се забавя за дейлито. Започнете без Мен.Aneliva Angelova 9:43 AMIДобро утро, няма да успея да вляза влейлито. Пествам ньлжовете.Message & platform-inner-team+ Aa I..•) New TabAl reports promotion pages by nik• JY-9712 | Nuges to expire after on8 Jiminnyu Userpilot Logged-activityJY-20157 add not enough activ XPipelines - jiminny/app+ New Tab©github.com/jimjiminny / app 8<> Code87 Pull requests 31( Agents |© Actions•• Wiki © Security and quality 32 ~ Insights 3 Settings@ On April 24 we'll start using GitHub Copilot interaction data for Al model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.JY-20157 add not enough activities notification #12011 •$1 Open LakyLak wants to merge 2 commits into master from JY-20157-AJ-report-not-send-notification@) Conversation o• Commits 2|- Checks 21E Files changed 13A © All commits +Q Filter files...apo/Console/Commands/Reports/AutomatedReportsCommand.ohp@ -61,21 +61,29 @ public function handle(): intv = Console/Commands/Renorts|Snow = Carbon: : now();E AutomatedReportsCommand...v Jobs/AutomatedReportsE RequestGenerateAskJiminnyR...SendReportNotGeneratedMail...v @ Listeners/AutomatedReports/U….E TrackAutomatedReportGener...v # Mail/ReportsS1sMondav = Snow->1SMonday)S1sr1rstDayUtMonth = Snow->day === 1;ScurrentMonth = Snow->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', [+ ReportNotGenerated.ohp |"1SMonday' => S1SMonday,~ E Services/Kiosk/AutomatedRepo…..AskJiminnyReportActivityServ…'isFirstDay0fMonth' => SisFirstDay0fMonth,'currentMonth' => ScurrentMonth.E AutomatedReportsService.php'isQuarterlyMonth' => SisQuarterlyMonth,~E resources/views/emails/reportsreport-not-generated.blade.php/I Process dailv revortsl• F tests/UnitSthis->processReports(AutomatedReportsService::FREQUENCYDAILY):~ Jobs/AutomatedReportsE ReguestGenerateAsk JiminnvR....v = listeners/AutomatedRenorts/U.₴ TrackAutomatedReportGener..v E Services/Kiosk/AutomatedRepo…..E AskJiminnyReportActivityServ....AutomatedReportsServiceActi…./ Process weekly renorts on Mondavcif (SisMondav) {64 +67 +74 +86 +@40@ Daily - Platform - nowQ Type to search100% C4 8• Fri 24 Apr 9:46:13• Checks pending Code • (Preview) -+384 -52 9000C 0 I 13 viewedSubmit review+10 -2 mane [ Viewed0 ...Snow = Carhon:.nowdSisMondav = Snow->1SMonday)"Sisweekend = $now->isWeekend():SisFirstDay0fMonth = Snow->day === 1;ScurrentMonth = Snow->month.SisManualTrigger = $this->option('report-id') !== null;// Check if the current month is a quarterly month (January, April, July, October)$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);Sthis->loager->info(self::L0G PREFIX . ' Checkina conditions'. [I"isMonday' => SisMonday,'isweekend' => $isWeekend,'isFirstDay0fMonth' => $isFirstDay0fMonth,'currentMonth' => ScurrentMonth.l'isQuarterlyMonth' => SisQuarterlyMonth,/ Process dailv renorts on weekdavs onlv (skio Saturdav/Sundav)...// Manual triggers via --report-id bypass the weekend skip.if (I Sisweekend || SisManualTriager) {Sthis->processReports(AutomatedReportsService::FREQUENCY_DAILY):} else {ISthis->logger->info(self::L0G PREFIX . ' Skipping daily reports on weekend'):/ Process weekly renorts on Mondavslif (SisMonday) {...
|
PhpStorm
|
faVsco.js – AutomatedReportsSendCommand.php
|
NULL
|
76746
|
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
ai-team
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stefka Stoyanova
Adelina Petrova
Vasil Vasilev
Stoyan Tomov
Galya Dimitrova
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 10:47:31 AM
10:47 AM
сега забелязах че стария count който се ползва в при контрол на достъп canAccessAiReport брои само ресултати който са пратени, не тези които са генерирани
Nikolay Yankov
Yesterday at 1:31:46 PM
1:31 PM
какво значи които са пратени?
Yesterday at 1:32:17 PM
1:32
по принцип условието беше - ако за user-a има генерирано и може да го гледа на ai-reports страницата
Lukas Kovalik
Yesterday at 1:35:32 PM
1:35 PM
да, говоря за нещо още от преди, count който се гледа дали има user право да гледа ai-reports страница , брои пратени не тези който са генерирани само. Реално ако се праща всичко на ред почти няма да се вижда разлика
(edited)
Jump to date
Aneliya Angelova
Today at 11:13:41 AM
11:13 AM
Лукаш вчера генерирах 3 седмични репорта на. Галя на prod us. Обаче не ги пуснах по емейл и тази сутрин ги е получила по мейл.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:15:05 AM
11:15 AM
седмични не са ли в понеделник само
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 11:15:19 AM
11:15 AM
да
React with white_check_mark
React with eyes
React with raised_hands
Firefox• 0FileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com•Daily - Platform - now100% K78 • Fri 24 Apr 9:46:13|=Pop out this videoNikolay NikolovStefka StoyanovaGalya DimitrovaLukas Kovalik9:46 AM | Daily - Platform• 0:27...
|
Slack
|
* Aneliya Angelova, Nikolay Yankov, Steliyan Georg * Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - Slack...
|
NULL
|
76747
|
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
ai-team
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stefka Stoyanova
Adelina Petrova
Vasil Vasilev
Stoyan Tomov
Galya Dimitrova
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 10:47:31 AM
10:47 AM
сега забелязах че стария count който се ползва в при контрол на достъп canAccessAiReport брои само ресултати който са пратени, не тези които са генерирани
Nikolay Yankov
Yesterday at 1:31:46 PM
1:31 PM
какво значи които са пратени?
Yesterday at 1:32:17 PM
1:32
по принцип условието беше - ако за user-a има генерирано и може да го гледа на ai-reports страницата
Lukas Kovalik
DMSActivityMorerireroxToolsHelpcalMistorbookmarksJiminny …..vXStarredi• jiminny-x-integrati..8 platform-inner-teamE) Channels# ai-chapter# ai-team# alerts# backend# c-learning-peoplei confusion-clinic# curiosity_labadeal-insichts-dev# engineering# frontend# general# infra-changes# jiminny-bg8 people-with-copilo...8 people-with-zoom-# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the Deople of iimi...ProtllesWindow& platform-inner-...& 10MessagesChannel OverviewMoreYesterdayjinnylaop Aor 22nd Added by GitHubNikolay Ivanov 3:24 PMнякой нещо да е настроивал по githubactions. Почна да прави къмити вместо менбез да съм му разрешевал?https:/github.com/lminnv/app/pull/1200//changes/a68f42f210859f838a4fdced451f750627besoioИли нещо аз не разбирам?0AA0e 20 replies Last reply 18...Nikolay Yankov 3:50PMreplied to a uhread: някои нешо ла е насто..лол. ами предлагам маи ла му заораним лапускам към всички ла вилятNikolav Yankov 9.38 AMЩе се забавя за дейлито. Започнете без Мен.Aneliva Angelova 9:43 AMIДобро утро, няма да успея да вляза влейлито. Пествам ньлжовете.Message & platform-inner-team+ Aa I..•) New TabAl reports promotion pages by nik• JY-9712 | Nuges to expire after on8 Jiminnyu Userpilot Logged-activityJY-20157 add not enough activ XPipelines - jiminny/app+ New Tab©github.com/jimjiminny / app 8<> Code87 Pull requests 31( Agents |© Actions•• Wiki © Security and quality 32 ~ Insights 3 Settings@ On April 24 we'll start using GitHub Copilot interaction data for Al model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.JY-20157 add not enough activities notification #12011 •$1 Open LakyLak wants to merge 2 commits into master from JY-20157-AJ-report-not-send-notification@) Conversation o• Commits 2|- Checks 21E Files changed 13A © All commits +Q Filter files...apo/Console/Commands/Reports/AutomatedReportsCommand.ohp@ -61,21 +61,29 @ public function handle(): intv = Console/Commands/Renorts|Snow = Carbon: : now();E AutomatedReportsCommand...v Jobs/AutomatedReportsE RequestGenerateAskJiminnyR...SendReportNotGeneratedMail...v @ Listeners/AutomatedReports/U….E TrackAutomatedReportGener...v # Mail/ReportsS1sMondav = Snow->1SMonday)S1sr1rstDayUtMonth = Snow->day === 1;ScurrentMonth = Snow->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', [+ ReportNotGenerated.ohp |"1SMonday' => S1SMonday,~ E Services/Kiosk/AutomatedRepo…..AskJiminnyReportActivityServ…'isFirstDay0fMonth' => SisFirstDay0fMonth,'currentMonth' => ScurrentMonth.E AutomatedReportsService.php'isQuarterlyMonth' => SisQuarterlyMonth,~E resources/views/emails/reportsreport-not-generated.blade.php/I Process dailv revortsl• F tests/UnitSthis->processReports(AutomatedReportsService::FREQUENCYDAILY):~ Jobs/AutomatedReportsE ReguestGenerateAsk JiminnvR....v = listeners/AutomatedRenorts/U.₴ TrackAutomatedReportGener..v E Services/Kiosk/AutomatedRepo…..E AskJiminnyReportActivityServ....AutomatedReportsServiceActi…./ Process weekly renorts on Mondavcif (SisMondav) {64 +67 +74 +86 +@40@ Daily - Platform - nowQ Type to search100% C4 8• Fri 24 Apr 9:46:13• Checks pending Code • (Preview) -+384 -52 9000C 0 I 13 viewedSubmit review+10 -2 mane [ Viewed0 ...Snow = Carhon:.nowdSisMondav = Snow->1SMonday)"Sisweekend = $now->isWeekend():SisFirstDay0fMonth = Snow->day === 1;ScurrentMonth = Snow->month.SisManualTrigger = $this->option('report-id') !== null;// Check if the current month is a quarterly month (January, April, July, October)$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);Sthis->loager->info(self::L0G PREFIX . ' Checkina conditions'. [I"isMonday' => SisMonday,'isweekend' => $isWeekend,'isFirstDay0fMonth' => $isFirstDay0fMonth,'currentMonth' => ScurrentMonth.l'isQuarterlyMonth' => SisQuarterlyMonth,/ Process dailv renorts on weekdavs onlv (skio Saturdav/Sundav)...// Manual triggers via --report-id bypass the weekend skip.if (I Sisweekend || SisManualTriager) {Sthis->processReports(AutomatedReportsService::FREQUENCY_DAILY):} else {ISthis->logger->info(self::L0G PREFIX . ' Skipping daily reports on weekend'):/ Process weekly renorts on Mondavslif (SisMonday) {...
|
Slack
|
* Aneliya Angelova, Nikolay Yankov, Steliyan Georg * Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - Slack...
|
NULL
|
76748
|
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
ai-team
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stefka Stoyanova
Adelina Petrova
Vasil Vasilev
Stoyan Tomov
Galya Dimitrova
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 10:47:31 AM
10:47 AM
сега забелязах че стария count който се ползва в при контрол на достъп canAccessAiReport брои само ресултати който са пратени, не тези които са генерирани
Nikolay Yankov
Yesterday at 1:31:46 PM
1:31 PM
какво значи които са пратени?
Yesterday at 1:32:17 PM
1:32
по принцип условието беше - ако за user-a има генерирано и може да го гледа на ai-reports страницата
Lukas Kovalik
Yesterday at 1:35:32 PM
1:35 PM
да, говоря за нещо още от преди, count който се гледа дали има user право да гледа ai-reports страница , брои пратени не тези който са генерирани само. Реално ако се праща всичко на ред почти няма да се вижда разлика
(edited)
Jump to date
Aneliya Angelova
Today at 11:13:41 AM
11:13 AM
Лукаш вчера генерирах 3 седмични репорта на. Галя на prod us. Обаче не ги пуснах по емейл и тази сутрин ги е получила по мейл.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:15:05 AM
11:15 AM
седмични не са ли в понеделник само
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 11:15:19 AM
11:15 AM
да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:15:56 AM
11:15
аз ги генерирах вчера. Да не би сутрин да минава джоба за разпращане на мейли и да събира всички генерирани репорти със статус 3 и да ги разпраща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:08 AM
11:16
без да се съобразява далки е дневен или седмичен или месечен
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:16:37 AM
11:16 AM
а да, за пращане мисля че няма проверки
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:56 AM
11:16
чакай да видя, мисля че само при генериране
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
Aneliya Angelova
Today at 11:18:33 AM
11:18 AM
Иначе ако това е обяснението - нека така си остане.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
да send cron job събира непратени ресултати
да send cron job събира непратени ресултати
Shift + Return to add a new line
Shift + Return
to add a new line
Adelina Petrova, Direct Message, 1 of 7 suggestions
Aneliya Angelova is typing
Firefox• 0FileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com•Daily - Platform - now100% K78 • Fri 24 Apr 9:46:13|=Pop out this videoNikolay NikolovStefka StoyanovaGalya DimitrovaLukas Kovalik9:46 AM | Daily - Platform• 0:27...
|
Slack
|
Aneliya Angelova, Nikolay Yankov, Steliyan Georgie Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - Slack...
|
NULL
|
76749
|
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
ai-team
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stefka Stoyanova
Adelina Petrova
Vasil Vasilev
Stoyan Tomov
Galya Dimitrova
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 10:47:31 AM
10:47 AM
сега забелязах че стария count който се ползва в при контрол на достъп canAccessAiReport брои само ресултати който са пратени, не тези които са генерирани
Nikolay Yankov
Yesterday at 1:31:46 PM
1:31 PM
какво значи които са пратени?
Yesterday at 1:32:17 PM
1:32
по принцип условието беше - ако за user-a има генерирано и може да го гледа на ai-reports страницата
Lukas Kovalik
Yesterday at 1:35:32 PM
1:35 PM
да, говоря за нещо още от преди, count който се гледа дали има user право да гледа ai-reports страница , брои пратени не тези който са генерирани само. Реално ако се праща всичко на ред почти няма да се вижда разлика
(edited)
Jump to date
Aneliya Angelova
Today at 11:13:41 AM
11:13 AM
Лукаш вчера генерирах 3 седмични репорта на. Галя на prod us. Обаче не ги пуснах по емейл и тази сутрин ги е получила по мейл.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:15:05 AM
11:15 AM
седмични не са ли в понеделник само
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 11:15:19 AM
11:15 AM
да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:15:56 AM
11:15
аз ги генерирах вчера. Да не би сутрин да минава джоба за разпращане на мейли и да събира всички генерирани репорти със статус 3 и да ги разпраща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:08 AM
11:16
без да се съобразява далки е дневен или седмичен или месечен
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:16:37 AM
11:16 AM
а да, за пращане мисля че няма проверки
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:56 AM
11:16
чакай да видя, мисля че само при генериране
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
Aneliya Angelova
Today at 11:18:33 AM
11:18 AM
Иначе ако това е обяснението - нека така си остане.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
да send cron job събира непратени ресултати
да send cron job събира непратени ресултати
Shift + Return to add a new line
Shift + Return
to add a new line
Adelina Petrova, Direct Message, 1 of 7 suggestions
Aneliya Angelova is typing
DMSActivityMorerireroxToolsHelpcalMistorbookmarksJiminny …..vXStarredi• jiminny-x-integrati..8 platform-inner-teamE) Channels# ai-chapter# ai-team# alerts# backend# c-learning-peoplei confusion-clinic# curiosity_labadeal-insichts-dev# engineering# frontend# general# infra-changes# jiminny-bg8 people-with-copilo...8 people-with-zoom-# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the Deople of iimi...ProtllesWindow& platform-inner-...& 10MessagesChannel OverviewMoreYesterdayjinnylaop Aor 22nd Added by GitHubNikolay Ivanov 3:24 PMнякой нещо да е настроивал по githubactions. Почна да прави къмити вместо менбез да съм му разрешевал?https:/github.com/lminnv/app/pull/1200//changes/a68f42f210859f838a4fdced451f750627besoioИли нещо аз не разбирам?0AA0e 20 replies Last reply 18...Nikolay Yankov 3:50PMreplied to a uhread: някои нешо ла е насто..лол. ами предлагам маи ла му заораним лапускам към всички ла вилятNikolav Yankov 9.38 AMЩе се забавя за дейлито. Започнете без Мен.Aneliva Angelova 9:43 AMIДобро утро, няма да успея да вляза влейлито. Пествам ньлжовете.Message & platform-inner-team+ Aa I..•) New TabAl reports promotion pages by nik• JY-9712 | Nuges to expire after on8 Jiminnyu Userpilot Logged-activityJY-20157 add not enough activ XPipelines - jiminny/app+ New Tab©github.com/jimjiminny / app 8<> Code87 Pull requests 31( Agents |© Actions•• Wiki © Security and quality 32 ~ Insights 3 Settings@ On April 24 we'll start using GitHub Copilot interaction data for Al model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.JY-20157 add not enough activities notification #12011 •$1 Open LakyLak wants to merge 2 commits into master from JY-20157-AJ-report-not-send-notification@) Conversation o• Commits 2|- Checks 21E Files changed 13A © All commits +Q Filter files...apo/Console/Commands/Reports/AutomatedReportsCommand.ohp@ -61,21 +61,29 @ public function handle(): intv = Console/Commands/Renorts|Snow = Carbon: : now();E AutomatedReportsCommand...v Jobs/AutomatedReportsE RequestGenerateAskJiminnyR...SendReportNotGeneratedMail...v @ Listeners/AutomatedReports/U….E TrackAutomatedReportGener...v # Mail/ReportsS1sMondav = Snow->1SMonday)S1sr1rstDayUtMonth = Snow->day === 1;ScurrentMonth = Snow->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', [+ ReportNotGenerated.ohp |"1SMonday' => S1SMonday,~ E Services/Kiosk/AutomatedRepo…..AskJiminnyReportActivityServ…'isFirstDay0fMonth' => SisFirstDay0fMonth,'currentMonth' => ScurrentMonth.E AutomatedReportsService.php'isQuarterlyMonth' => SisQuarterlyMonth,~E resources/views/emails/reportsreport-not-generated.blade.php/I Process dailv revortsl• F tests/UnitSthis->processReports(AutomatedReportsService::FREQUENCYDAILY):~ Jobs/AutomatedReportsE ReguestGenerateAsk JiminnvR....v = listeners/AutomatedRenorts/U.₴ TrackAutomatedReportGener..v E Services/Kiosk/AutomatedRepo…..E AskJiminnyReportActivityServ....AutomatedReportsServiceActi…./ Process weekly renorts on Mondavcif (SisMondav) {64 +67 +74 +86 +@40@ Daily - Platform - nowQ Type to search100% C4 8• Fri 24 Apr 9:46:13• Checks pending Code • (Preview) -+384 -52 9000C 0 I 13 viewedSubmit review+10 -2 mane [ Viewed0 ...Snow = Carhon:.nowdSisMondav = Snow->1SMonday)"Sisweekend = $now->isWeekend():SisFirstDay0fMonth = Snow->day === 1;ScurrentMonth = Snow->month.SisManualTrigger = $this->option('report-id') !== null;// Check if the current month is a quarterly month (January, April, July, October)$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);Sthis->loager->info(self::L0G PREFIX . ' Checkina conditions'. [I"isMonday' => SisMonday,'isweekend' => $isWeekend,'isFirstDay0fMonth' => $isFirstDay0fMonth,'currentMonth' => ScurrentMonth.l'isQuarterlyMonth' => SisQuarterlyMonth,/ Process dailv renorts on weekdavs onlv (skio Saturdav/Sundav)...// Manual triggers via --report-id bypass the weekend skip.if (I Sisweekend || SisManualTriager) {Sthis->processReports(AutomatedReportsService::FREQUENCY_DAILY):} else {ISthis->logger->info(self::L0G PREFIX . ' Skipping daily reports on weekend'):/ Process weekly renorts on Mondavslif (SisMonday) {...
|
Slack
|
Aneliya Angelova, Nikolay Yankov, Steliyan Georgie Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - Slack...
|
NULL
|
76750
|
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
ai-team
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stefka Stoyanova
Adelina Petrova
Vasil Vasilev
Stoyan Tomov
Galya Dimitrova
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 10:47:31 AM
10:47 AM
сега забелязах че стария count който се ползва в при контрол на достъп canAccessAiReport брои само ресултати който са пратени, не тези които са генерирани
Nikolay Yankov
Yesterday at 1:31:46 PM
1:31 PM
какво значи които са пратени?
Yesterday at 1:32:17 PM
1:32
по принцип условието беше - ако за user-a има генерирано и може да го гледа на ai-reports страницата
Lukas Kovalik
Yesterday at 1:35:32 PM
1:35 PM
да, говоря за нещо още от преди, count който се гледа дали има user право да гледа ai-reports страница , брои пратени не тези който са генерирани само. Реално ако се праща всичко на ред почти няма да се вижда разлика
(edited)
Jump to date
Aneliya Angelova
Today at 11:13:41 AM
11:13 AM
Лукаш вчера генерирах 3 седмични репорта на. Галя на prod us. Обаче не ги пуснах по емейл и тази сутрин ги е получила по мейл.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:15:05 AM
11:15 AM
седмични не са ли в понеделник само
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 11:15:19 AM
11:15 AM
да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:15:56 AM
11:15
аз ги генерирах вчера. Да не би сутрин да минава джоба за разпращане на мейли и да събира всички генерирани репорти със статус 3 и да ги разпраща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:08 AM
11:16
без да се съобразява далки е дневен или седмичен или месечен
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:16:37 AM
11:16 AM
а да, за пращане мисля че няма проверки
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:56 AM
11:16
чакай да видя, мисля че само при генериране
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
Aneliya Angelova
Today at 11:18:33 AM
11:18 AM
Иначе ако това е обяснението - нека така си остане.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
да send cron job събира неизпратени ресултати
да send cron job събира неизпратени ресултати
Shift + Return to add a new line
Shift + Return
to add a new line
Adelina Petrova, Direct Message, 1 of 7 suggestions
Aneliya Angelova is typing
Firefox• 0FileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com•Daily - Platform - now100% K78 • Fri 24 Apr 9:46:13|=Pop out this videoNikolay NikolovStefka StoyanovaGalya DimitrovaLukas Kovalik9:46 AM | Daily - Platform• 0:27...
|
Slack
|
Aneliya Angelova, Nikolay Yankov, Steliyan Georgie Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - Slack...
|
NULL
|
76751
|
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
ai-team
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stefka Stoyanova
Adelina Petrova
Vasil Vasilev
Stoyan Tomov
Galya Dimitrova
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 10:47:31 AM
10:47 AM
сега забелязах че стария count който се ползва в при контрол на достъп canAccessAiReport брои само ресултати който са пратени, не тези които са генерирани
Nikolay Yankov
Yesterday at 1:31:46 PM
1:31 PM
какво значи които са пратени?
Yesterday at 1:32:17 PM
1:32
по принцип условието беше - ако за user-a има генерирано и може да го гледа на ai-reports страницата
Lukas Kovalik
Yesterday at 1:35:32 PM
1:35 PM
да, говоря за нещо още от преди, count който се гледа дали има user право да гледа ai-reports страница , брои пратени не тези който са генерирани само. Реално ако се праща всичко на ред почти няма да се вижда разлика
(edited)
Jump to date
Aneliya Angelova
Today at 11:13:41 AM
11:13 AM
DMSActivityMorerireroxToolsHelpcalMistorbookmarksJiminny …..vXStarredi• jiminny-x-integrati..8 platform-inner-teamE) Channels# ai-chapter# ai-team# alerts# backend# c-learning-peoplei confusion-clinic# curiosity_labadeal-insichts-dev# engineering# frontend# general# infra-changes# jiminny-bg8 people-with-copilo...8 people-with-zoom-# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the Deople of iimi...ProtllesWindow& platform-inner-...& 10MessagesChannel OverviewMoreYesterdayjinnylaop Aor 22nd Added by GitHubNikolay Ivanov 3:24 PMнякой нещо да е настроивал по githubactions. Почна да прави къмити вместо менбез да съм му разрешевал?https:/github.com/lminnv/app/pull/1200//changes/a68f42f210859f838a4fdced451f750627besoioИли нещо аз не разбирам?0AA0e 20 replies Last reply 18...Nikolay Yankov 3:50PMreplied to a uhread: някои нешо ла е насто..лол. ами предлагам маи ла му заораним лапускам към всички ла вилятNikolav Yankov 9.38 AMЩе се забавя за дейлито. Започнете без Мен.Aneliva Angelova 9:43 AMIДобро утро, няма да успея да вляза влейлито. Пествам ньлжовете.Message & platform-inner-team+ Aa I..•) New TabAl reports promotion pages by nik• JY-9712 | Nuges to expire after on8 Jiminnyu Userpilot Logged-activityJY-20157 add not enough activ XPipelines - jiminny/app+ New Tab©github.com/jimjiminny / app 8<> Code87 Pull requests 31( Agents |© Actions•• Wiki © Security and quality 32 ~ Insights 3 Settings@ On April 24 we'll start using GitHub Copilot interaction data for Al model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.JY-20157 add not enough activities notification #12011 •$1 Open LakyLak wants to merge 2 commits into master from JY-20157-AJ-report-not-send-notification@) Conversation o• Commits 2|- Checks 21E Files changed 13A © All commits +Q Filter files...apo/Console/Commands/Reports/AutomatedReportsCommand.ohp@ -61,21 +61,29 @ public function handle(): intv = Console/Commands/Renorts|Snow = Carbon: : now();E AutomatedReportsCommand...v Jobs/AutomatedReportsE RequestGenerateAskJiminnyR...SendReportNotGeneratedMail...v @ Listeners/AutomatedReports/U….E TrackAutomatedReportGener...v # Mail/ReportsS1sMondav = Snow->1SMonday)S1sr1rstDayUtMonth = Snow->day === 1;ScurrentMonth = Snow->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', [+ ReportNotGenerated.ohp |"1SMonday' => S1SMonday,~ E Services/Kiosk/AutomatedRepo…..AskJiminnyReportActivityServ…'isFirstDay0fMonth' => SisFirstDay0fMonth,'currentMonth' => ScurrentMonth.E AutomatedReportsService.php'isQuarterlyMonth' => SisQuarterlyMonth,~E resources/views/emails/reportsreport-not-generated.blade.php/I Process dailv revortsl• F tests/UnitSthis->processReports(AutomatedReportsService::FREQUENCYDAILY):~ Jobs/AutomatedReportsE ReguestGenerateAsk JiminnvR....v = listeners/AutomatedRenorts/U.₴ TrackAutomatedReportGener..v E Services/Kiosk/AutomatedRepo…..E AskJiminnyReportActivityServ....AutomatedReportsServiceActi…./ Process weekly renorts on Mondavcif (SisMondav) {64 +67 +74 +86 +@40@ Daily - Platform - nowQ Type to search100% C4 8• Fri 24 Apr 9:46:13• Checks pending Code • (Preview) -+384 -52 9000C 0 I 13 viewedSubmit review+10 -2 mane [ Viewed0 ...Snow = Carhon:.nowdSisMondav = Snow->1SMonday)"Sisweekend = $now->isWeekend():SisFirstDay0fMonth = Snow->day === 1;ScurrentMonth = Snow->month.SisManualTrigger = $this->option('report-id') !== null;// Check if the current month is a quarterly month (January, April, July, October)$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);Sthis->loager->info(self::L0G PREFIX . ' Checkina conditions'. [I"isMonday' => SisMonday,'isweekend' => $isWeekend,'isFirstDay0fMonth' => $isFirstDay0fMonth,'currentMonth' => ScurrentMonth.l'isQuarterlyMonth' => SisQuarterlyMonth,/ Process dailv renorts on weekdavs onlv (skio Saturdav/Sundav)...// Manual triggers via --report-id bypass the weekend skip.if (I Sisweekend || SisManualTriager) {Sthis->processReports(AutomatedReportsService::FREQUENCY_DAILY):} else {ISthis->logger->info(self::L0G PREFIX . ' Skipping daily reports on weekend'):/ Process weekly renorts on Mondavslif (SisMonday) {...
|
Slack
|
Aneliya Angelova, Nikolay Yankov, Steliyan Georgie Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - Slack...
|
NULL
|
76752
|
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
ai-team
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stefka Stoyanova
Adelina Petrova
Vasil Vasilev
Stoyan Tomov
Galya Dimitrova
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 10:47:31 AM
10:47 AM
сега забелязах че стария count който се ползва в при контрол на достъп canAccessAiReport брои само ресултати който са пратени, не тези които са генерирани
Nikolay Yankov
Yesterday at 1:31:46 PM
1:31 PM
какво значи които са пратени?
Yesterday at 1:32:17 PM
1:32
по принцип условието беше - ако за user-a има генерирано и може да го гледа на ai-reports страницата
Lukas Kovalik
Yesterday at 1:35:32 PM
1:35 PM
да, говоря за нещо още от преди, count който се гледа дали има user право да гледа ai-reports страница , брои пратени не тези който са генерирани само. Реално ако се праща всичко на ред почти няма да се вижда разлика
(edited)
Jump to date
Aneliya Angelova
Today at 11:13:41 AM
11:13 AM
Лукаш вчера генерирах 3 седмични репорта на. Галя на prod us. Обаче не ги пуснах по емейл и тази сутрин ги е получила по мейл.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:15:05 AM
11:15 AM
седмични не са ли в понеделник само
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 11:15:19 AM
11:15 AM
да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:15:56 AM
11:15
аз ги генерирах вчера. Да не би сутрин да минава джоба за разпращане на мейли и да събира всички генерирани репорти със статус 3 и да ги разпраща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:08 AM
11:16
без да се съобразява далки е дневен или седмичен или месечен
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:16:37 AM
11:16 AM
а да, за пращане мисля че няма проверки
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:56 AM
11:16
чакай да видя, мисля че само при генериране
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
Aneliya Angelova
Today at 11:18:33 AM
11:18 AM
Иначе ако това е обяснението - нека така си остане.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:20:59 AM
11:20 AM
да send cron job събира неизпратени ресултати
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
и преценява само по тимезоне
Firefox• 0FileEditViewHistory→BookmarksProfilesToolsWindowHelpmeet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com•Daily - Platform - now100% K78 • Fri 24 Apr 9:46:13|=Pop out this videoNikolay NikolovStefka StoyanovaGalya DimitrovaLukas Kovalik9:46 AM | Daily - Platform• 0:27...
|
Slack
|
Aneliya Angelova, Nikolay Yankov, Steliyan Georgie Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - Slack...
|
NULL
|
76753
|
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
ai-team
alerts
backend
c-learning-people
confusion-clinic
curiosity_lab
deal-insights-dev
engineering
frontend
general
infra-changes
jiminny-bg
people-with-copilot-licences
people-with-zoom-phone-licences
platform-team
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stefka Stoyanova
Adelina Petrova
Vasil Vasilev
Stoyan Tomov
Galya Dimitrova
Nikolay Yankov
Petko Kashinski
Aneliya Angelova
Nikolay Nikolov
Mario Georgiev
Todor Stamatov
Gabriela Dureva
Jira Cloud
Toast
Messages
Messages
Add canvas
Add canvas
Files
Files
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Lukas Kovalik
Yesterday at 10:47:31 AM
10:47 AM
сега забелязах че стария count който се ползва в при контрол на достъп canAccessAiReport брои само ресултати който са пратени, не тези които са генерирани
Nikolay Yankov
Yesterday at 1:31:46 PM
1:31 PM
какво значи които са пратени?
Yesterday at 1:32:17 PM
1:32
по принцип условието беше - ако за user-a има генерирано и може да го гледа на ai-reports страницата
Lukas Kovalik
Yesterday at 1:35:32 PM
1:35 PM
да, говоря за нещо още от преди, count който се гледа дали има user право да гледа ai-reports страница , брои пратени не тези който са генерирани само. Реално ако се праща всичко на ред почти няма да се вижда разлика
(edited)
Jump to date
Aneliya Angelova
Today at 11:13:41 AM
11:13 AM
Лукаш вчера генерирах 3 седмични репорта на. Галя на prod us. Обаче не ги пуснах по емейл и тази сутрин ги е получила по мейл.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:15:05 AM
11:15 AM
седмични не са ли в понеделник само
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Aneliya Angelova
Today at 11:15:19 AM
11:15 AM
да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:15:56 AM
11:15
аз ги генерирах вчера. Да не би сутрин да минава джоба за разпращане на мейли и да събира всички генерирани репорти със статус 3 и да ги разпраща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:08 AM
11:16
без да се съобразява далки е дневен или седмичен или месечен
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:16:37 AM
11:16 AM
а да, за пращане мисля че няма проверки
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 11:16:56 AM
11:16
чакай да видя, мисля че само при генериране
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
New
Aneliya Angelova
Today at 11:18:33 AM
11:18 AM
Иначе ако това е обяснението - нека така си остане.
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 11:20:59 AM
11:20 AM
да send cron job събира неизпратени ресултати
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
и преценява само по timezone
и преценява само по timezone
Shift + Return to add a new line
Shift + Return
to add a new line
Adelina Petrova, Direct Message, 1 of 7 suggestions
Aneliya Angelova is typing
DMSActivityMorerireroxToolsHelpcalMistorbookmarksJiminny …..vXStarredi• jiminny-x-integrati..8 platform-inner-teamE) Channels# ai-chapter# ai-team# alerts# backend# c-learning-peoplei confusion-clinic# curiosity_labadeal-insichts-dev# engineering# frontend# general# infra-changes# jiminny-bg8 people-with-copilo...8 people-with-zoom-# platform-team# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the Deople of iimi...ProtllesWindow& platform-inner-...& 10MessagesChannel OverviewMoreYesterdayjinnylaop Aor 22nd Added by GitHubNikolay Ivanov 3:24 PMнякой нещо да е настроивал по githubactions. Почна да прави къмити вместо менбез да съм му разрешевал?https:/github.com/lminnv/app/pull/1200//changes/a68f42f210859f838a4fdced451f750627besoioИли нещо аз не разбирам?0AA0e 20 replies Last reply 18...Nikolay Yankov 3:50PMreplied to a uhread: някои нешо ла е насто..лол. ами предлагам маи ла му заораним лапускам към всички ла вилятNikolav Yankov 9.38 AMЩе се забавя за дейлито. Започнете без Мен.Aneliva Angelova 9:43 AMIДобро утро, няма да успея да вляза влейлито. Пествам ньлжовете.Message & platform-inner-team+ Aa I..•) New TabAl reports promotion pages by nik• JY-9712 | Nuges to expire after on8 Jiminnyu Userpilot Logged-activityJY-20157 add not enough activ XPipelines - jiminny/app+ New Tab©github.com/jimjiminny / app 8<> Code87 Pull requests 31( Agents |© Actions•• Wiki © Security and quality 32 ~ Insights 3 Settings@ On April 24 we'll start using GitHub Copilot interaction data for Al model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.JY-20157 add not enough activities notification #12011 •$1 Open LakyLak wants to merge 2 commits into master from JY-20157-AJ-report-not-send-notification@) Conversation o• Commits 2|- Checks 21E Files changed 13A © All commits +Q Filter files...apo/Console/Commands/Reports/AutomatedReportsCommand.ohp@ -61,21 +61,29 @ public function handle(): intv = Console/Commands/Renorts|Snow = Carbon: : now();E AutomatedReportsCommand...v Jobs/AutomatedReportsE RequestGenerateAskJiminnyR...SendReportNotGeneratedMail...v @ Listeners/AutomatedReports/U….E TrackAutomatedReportGener...v # Mail/ReportsS1sMondav = Snow->1SMonday)S1sr1rstDayUtMonth = Snow->day === 1;ScurrentMonth = Snow->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', [+ ReportNotGenerated.ohp |"1SMonday' => S1SMonday,~ E Services/Kiosk/AutomatedRepo…..AskJiminnyReportActivityServ…'isFirstDay0fMonth' => SisFirstDay0fMonth,'currentMonth' => ScurrentMonth.E AutomatedReportsService.php'isQuarterlyMonth' => SisQuarterlyMonth,~E resources/views/emails/reportsreport-not-generated.blade.php/I Process dailv revortsl• F tests/UnitSthis->processReports(AutomatedReportsService::FREQUENCYDAILY):~ Jobs/AutomatedReportsE ReguestGenerateAsk JiminnvR....v = listeners/AutomatedRenorts/U.₴ TrackAutomatedReportGener..v E Services/Kiosk/AutomatedRepo…..E AskJiminnyReportActivityServ....AutomatedReportsServiceActi…./ Process weekly renorts on Mondavcif (SisMondav) {64 +67 +74 +86 +@40@ Daily - Platform - nowQ Type to search100% C4 8• Fri 24 Apr 9:46:13• Checks pending Code • (Preview) -+384 -52 9000C 0 I 13 viewedSubmit review+10 -2 mane [ Viewed0 ...Snow = Carhon:.nowdSisMondav = Snow->1SMonday)"Sisweekend = $now->isWeekend():SisFirstDay0fMonth = Snow->day === 1;ScurrentMonth = Snow->month.SisManualTrigger = $this->option('report-id') !== null;// Check if the current month is a quarterly month (January, April, July, October)$isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);Sthis->loager->info(self::L0G PREFIX . ' Checkina conditions'. [I"isMonday' => SisMonday,'isweekend' => $isWeekend,'isFirstDay0fMonth' => $isFirstDay0fMonth,'currentMonth' => ScurrentMonth.l'isQuarterlyMonth' => SisQuarterlyMonth,/ Process dailv renorts on weekdavs onlv (skio Saturdav/Sundav)...// Manual triggers via --report-id bypass the weekend skip.if (I Sisweekend || SisManualTriager) {Sthis->processReports(AutomatedReportsService::FREQUENCY_DAILY):} else {ISthis->logger->info(self::L0G PREFIX . ' Skipping daily reports on weekend'):/ Process weekly renorts on Mondavslif (SisMonday) {...
|
Slack
|
Aneliya Angelova, Nikolay Yankov, Steliyan Georgie Aneliya Angelova, Nikolay Yankov, Steliyan Georgiev (DM) - Jiminny Inc - Slack...
|
NULL
|
76754
|