|
11070
|
219
|
26
|
2026-04-14T09:11:18.145849+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776157878145_m2.jpg...
|
PhpStorm
|
faVsco.js – OnDemandV2Controller.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app
.circleci
.cursor
.github
.sonarlint
.vscode
.windsurf
app, sources root
Actions
Component
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
EventSubscriber, folder
FilterDefinition, folder
Service
ActivityApiSearch.php, class
ActivitySearch.php, class
UserOptionsByGroup.php, class
AbstractStageFilterDefinition.php, abstract class
ActivitySearchServiceProvider.php, final class
DealInsightsPeriodFilterFactory.php
DealInsightsPeriodFilterFactoryInterface.php, interface
FilterDefinition.php, abstract class
FilterDefinitionCollection.php, class
FilterDefinitionQuery.php, class
FilterDefinitionQueryCollection.php, class
FilteredValueContainerInterface.php, interface
IntMinMaxRange.php, class
AiActivityType
AiAutomation
AiCallScoring
AskAnything
AskJiminnyAi
AWS
BillingManagement
Cache
CoachingFeedback
Country
CustomerApi
Database
Datadog
DateTime
DealInsights
DealRisks
ElasticSearch
Eloquent
Encoding
Encryption
ES
Faker
FeatureFlags
FFMpeg
FileSystem
Gecko
Gong
GuzzleHttp
KeyPoints
Kiosk
LanguageDetection
LiveFeed
Locks
Math
MediaPipeline
MeetingBot
MobileSettings
Model
Notification
Nudge
ParagraphBreaker
ParticipantSpeech
PartitionedCookie
PlaybackPage
Playlist
Prophet
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder
Slack, folder
TeamInsights, folder
TimeMemoryMapper, folder
Transcription, folder
TranscriptionSummary, folder
Twilio, folder
Uploader, folder
UrlGenerator, folder
Utility, folder
Uuid, folder
Waveform, folder
Webhooks, folder
Workflow, folder
Configuration
Console
Commands
Activities
Analytics
Calendars
Crm
Hubspot
IntegrationApp
AddLayoutEntities.php, class
AutologDelayedCommand.php, class
BullhornCommandAbstract.php, abstract class
BullhornPingCommand.php, class
BullhornSearchCommand.php, class
BullhornSessionCommand.php, class
CheckActivityLoggableCommand.php, final class
CleanDuplicateFieldDataCommand.php, class
FullSyncOpportunityCommand.php, class
LogActivitiesCommand.php, final class
ManageSyncStrategyCommand.php, class
MatchCrmObjectsCommand.php, class
MatchOpportunityActivitiesCommand.php, class
MigrateProvider.php, class
ProcessHubspotObjectsSyncBatches.php, class
PurgeDeletedOpportunitiesCommand.php, class
ResetGovernorLimits.php, class
SendNotLogged.php, class
SetupActivityTypeForFollowUp.php, final class
SetupCloseCrm.php, class
SetupCopperCrm.php, class
SetupCrmCommand.php, abstract class
SetupLayouts.php, class
SyncAccount.php, class
SyncContact.php, class
SyncFieldMetadata.php, class
SyncHubspotActiveDeals.php, class
SyncLead.php, class
SyncObjects.php, class
SyncOpportunitiesMissingFieldDataCommand.php, class
SyncOpportunity.php, class
SyncProfileMetadata.php, class
SyncTeamMetadata.php, class
UpdateOpportunitySpecifications.php, class
DealInsights
Dev
Dialers
DTOs
Elasticsearch
EngagementStats
GeckoExport
Livestream
Mailboxes
Migrate
PlaybackThemes
Playbooks
Playlists
Postmark
ProphetAi
Reports
AutomatedReportsCommand.php, class
AutomatedReportsRetentionPolicyCommand.php, class
AutomatedReportsSendCommand.php, class
CreateMockAskJiminnyReportResultCommand.php, class
DeleteReportCommand.php, class
GenerateMarketingReport.php, class
Team.php, class
Usage.php, class...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.57539064,"top":0.13055556,"width":0.00859375,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.58632815,"top":0.13055556,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.59765625,"top":0.12916666,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.60625,"top":0.12916666,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.2589844,"top":0.28125,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.27265626,"top":0.28125,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.28398436,"top":0.27986112,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.29257813,"top":0.27986112,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app","depth":6,"role_description":"text"},{"role":"AXStaticText","text":".circleci","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".cursor","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".vscode","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".windsurf","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Actions","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Component","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"EventSubscriber, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"FilterDefinition, folder","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Service","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"ActivityApiSearch.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"UserOptionsByGroup.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"AbstractStageFilterDefinition.php, abstract class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearchServiceProvider.php, final class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"DealInsightsPeriodFilterFactory.php","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"DealInsightsPeriodFilterFactoryInterface.php, interface","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"FilterDefinition.php, abstract class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"FilterDefinitionCollection.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"FilterDefinitionQuery.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"FilterDefinitionQueryCollection.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"FilteredValueContainerInterface.php, interface","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"IntMinMaxRange.php, class","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"AWS","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Cache","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Country","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Database","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Datadog","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"DateTime","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Encoding","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Encryption","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ES","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Faker","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Gecko","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Gong","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Locks","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Math","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"MeetingBot","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"MobileSettings","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Model","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Notification","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Nudge","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ParagraphBreaker","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ParticipantSpeech","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"PartitionedCookie","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackPage","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Playlist","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Prophet","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ProsperWorks, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Queue, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Router, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Saml2, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"SCIM, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Seeder, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Sentry, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Serializer, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Settings, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Sidekick, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"TimeMemoryMapper, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"TranscriptionSummary, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Uploader, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"UrlGenerator, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Utility, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Uuid, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Waveform, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Webhooks, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Workflow, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Configuration","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Console","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Commands","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Activities","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Analytics","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Calendars","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Crm","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Hubspot","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"IntegrationApp","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"AddLayoutEntities.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"AutologDelayedCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"BullhornCommandAbstract.php, abstract class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"BullhornPingCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSearchCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSessionCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"CheckActivityLoggableCommand.php, final class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"CleanDuplicateFieldDataCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"FullSyncOpportunityCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"LogActivitiesCommand.php, final class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"ManageSyncStrategyCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmObjectsCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"MatchOpportunityActivitiesCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"MigrateProvider.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"ProcessHubspotObjectsSyncBatches.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"PurgeDeletedOpportunitiesCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"ResetGovernorLimits.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SendNotLogged.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SetupActivityTypeForFollowUp.php, final class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SetupCloseCrm.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SetupCopperCrm.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SetupCrmCommand.php, abstract class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SetupLayouts.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SyncAccount.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SyncContact.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SyncFieldMetadata.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SyncHubspotActiveDeals.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SyncLead.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SyncObjects.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SyncOpportunitiesMissingFieldDataCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SyncOpportunity.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SyncProfileMetadata.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"SyncTeamMetadata.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"UpdateOpportunitySpecifications.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Dev","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Dialers","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"DTOs","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Elasticsearch","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"EngagementStats","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"GeckoExport","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Livestream","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Mailboxes","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Migrate","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackThemes","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Playbooks","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Playlists","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Postmark","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"Reports","depth":10,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsRetentionPolicyCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsSendCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"CreateMockAskJiminnyReportResultCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"DeleteReportCommand.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"GenerateMarketingReport.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"Team.php, class","depth":11,"role_description":"text"},{"role":"AXStaticText","text":"Usage.php, class","depth":11,"role_description":"text"}]...
|
-1792450594297658947
|
-5824280404149933980
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app
.circleci
.cursor
.github
.sonarlint
.vscode
.windsurf
app, sources root
Actions
Component
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
EventSubscriber, folder
FilterDefinition, folder
Service
ActivityApiSearch.php, class
ActivitySearch.php, class
UserOptionsByGroup.php, class
AbstractStageFilterDefinition.php, abstract class
ActivitySearchServiceProvider.php, final class
DealInsightsPeriodFilterFactory.php
DealInsightsPeriodFilterFactoryInterface.php, interface
FilterDefinition.php, abstract class
FilterDefinitionCollection.php, class
FilterDefinitionQuery.php, class
FilterDefinitionQueryCollection.php, class
FilteredValueContainerInterface.php, interface
IntMinMaxRange.php, class
AiActivityType
AiAutomation
AiCallScoring
AskAnything
AskJiminnyAi
AWS
BillingManagement
Cache
CoachingFeedback
Country
CustomerApi
Database
Datadog
DateTime
DealInsights
DealRisks
ElasticSearch
Eloquent
Encoding
Encryption
ES
Faker
FeatureFlags
FFMpeg
FileSystem
Gecko
Gong
GuzzleHttp
KeyPoints
Kiosk
LanguageDetection
LiveFeed
Locks
Math
MediaPipeline
MeetingBot
MobileSettings
Model
Notification
Nudge
ParagraphBreaker
ParticipantSpeech
PartitionedCookie
PlaybackPage
Playlist
Prophet
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder
Slack, folder
TeamInsights, folder
TimeMemoryMapper, folder
Transcription, folder
TranscriptionSummary, folder
Twilio, folder
Uploader, folder
UrlGenerator, folder
Utility, folder
Uuid, folder
Waveform, folder
Webhooks, folder
Workflow, folder
Configuration
Console
Commands
Activities
Analytics
Calendars
Crm
Hubspot
IntegrationApp
AddLayoutEntities.php, class
AutologDelayedCommand.php, class
BullhornCommandAbstract.php, abstract class
BullhornPingCommand.php, class
BullhornSearchCommand.php, class
BullhornSessionCommand.php, class
CheckActivityLoggableCommand.php, final class
CleanDuplicateFieldDataCommand.php, class
FullSyncOpportunityCommand.php, class
LogActivitiesCommand.php, final class
ManageSyncStrategyCommand.php, class
MatchCrmObjectsCommand.php, class
MatchOpportunityActivitiesCommand.php, class
MigrateProvider.php, class
ProcessHubspotObjectsSyncBatches.php, class
PurgeDeletedOpportunitiesCommand.php, class
ResetGovernorLimits.php, class
SendNotLogged.php, class
SetupActivityTypeForFollowUp.php, final class
SetupCloseCrm.php, class
SetupCopperCrm.php, class
SetupCrmCommand.php, abstract class
SetupLayouts.php, class
SyncAccount.php, class
SyncContact.php, class
SyncFieldMetadata.php, class
SyncHubspotActiveDeals.php, class
SyncLead.php, class
SyncObjects.php, class
SyncOpportunitiesMissingFieldDataCommand.php, class
SyncOpportunity.php, class
SyncProfileMetadata.php, class
SyncTeamMetadata.php, class
UpdateOpportunitySpecifications.php, class
DealInsights
Dev
Dialers
DTOs
Elasticsearch
EngagementStats
GeckoExport
Livestream
Mailboxes
Migrate
PlaybackThemes
Playbooks
Playlists
Postmark
ProphetAi
Reports
AutomatedReportsCommand.php, class
AutomatedReportsRetentionPolicyCommand.php, class
AutomatedReportsSendCommand.php, class
CreateMockAskJiminnyReportResultCommand.php, class
DeleteReportCommand.php, class
GenerateMarketingReport.php, class
Team.php, class
Usage.php, class...
|
11069
|
|
11073
|
218
|
23
|
2026-04-14T09:11:29.975613+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776157889975_m1.jpg...
|
PhpStorm
|
faVsco.js – OnDemandV2Controller.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5923402359908557185
|
-5824280404149966624
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11071
|
|
11087
|
218
|
31
|
2026-04-14T09:12:16.523620+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776157936523_m1.jpg...
|
PhpStorm
|
faVsco.js – OnDemandV2Controller.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5923402359908557185
|
-5824280404149966624
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11085
|
|
11088
|
219
|
34
|
2026-04-14T09:12:16.565275+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776157936565_m2.jpg...
|
PhpStorm
|
faVsco.js – OnDemandV2Controller.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app
.circleci
.cursor
.github
.sonarlint
.vscode
.windsurf
app, sources root
Actions
Component...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.57539064,"top":0.13055556,"width":0.00859375,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.58632815,"top":0.13055556,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.59765625,"top":0.12916666,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.60625,"top":0.12916666,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.2589844,"top":0.28125,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.27265626,"top":0.28125,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.28398436,"top":0.27986112,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.29257813,"top":0.27986112,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app","depth":6,"role_description":"text"},{"role":"AXStaticText","text":".circleci","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".cursor","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".vscode","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".windsurf","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Actions","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Component","depth":8,"role_description":"text"}]...
|
-149015398497082264
|
-5824280399854999452
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app
.circleci
.cursor
.github
.sonarlint
.vscode
.windsurf
app, sources root
Actions
Component...
|
11086
|
|
11092
|
218
|
34
|
2026-04-14T09:12:28.131122+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776157948131_m1.jpg...
|
PhpStorm
|
faVsco.js – OnDemandV2Controller.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5923402359908557185
|
-5824280404149966624
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
11093
|
219
|
36
|
2026-04-14T09:12:28.114430+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776157948114_m2.jpg...
|
PhpStorm
|
faVsco.js – OnDemandV2Controller.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.57539064,"top":0.13055556,"width":0.00859375,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.58632815,"top":0.13055556,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.59765625,"top":0.12916666,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.60625,"top":0.12916666,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.2589844,"top":0.28125,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.27265626,"top":0.28125,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.28398436,"top":0.27986112,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.29257813,"top":0.27986112,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5923402359908557185
|
-5824280404149966624
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11090
|
|
11110
|
218
|
43
|
2026-04-14T09:13:17.689058+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776157997689_m1.jpg...
|
PhpStorm
|
faVsco.js – OnDemandV2Controller.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
activitySearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Replace Field","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"activitySearch","depth":4,"value":"activitySearch","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/2","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-661265807901631934
|
-5802339713239498621
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
activitySearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}...
|
11108
|
|
11111
|
219
|
45
|
2026-04-14T09:13:17.692160+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776157997692_m2.jpg...
|
PhpStorm
|
faVsco.js – OnDemandV2Controller.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
activitySearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Replace Field","depth":4,"bounds":{"left":0.30546874,"top":0.13472222,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.3203125,"top":0.13402778,"width":0.00859375,"height":0.015277778},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"activitySearch","depth":4,"bounds":{"left":0.33320314,"top":0.13402778,"width":0.0515625,"height":0.013888889},"value":"activitySearch","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.3953125,"top":0.13402778,"width":0.00859375,"height":0.015277778},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"bounds":{"left":0.40703124,"top":0.13402778,"width":0.00859375,"height":0.015277778},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"bounds":{"left":0.4171875,"top":0.13402778,"width":0.00859375,"height":0.015277778},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"bounds":{"left":0.42734376,"top":0.13402778,"width":0.00859375,"height":0.015277778},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/2","depth":4,"bounds":{"left":0.44335938,"top":0.13333334,"width":0.030078124,"height":0.015277778},"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"bounds":{"left":0.4734375,"top":0.13263889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"bounds":{"left":0.48359376,"top":0.13263889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"bounds":{"left":0.49375,"top":0.13263889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"bounds":{"left":0.50390625,"top":0.13263889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"bounds":{"left":0.5992187,"top":0.13263889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.57539064,"top":0.15972222,"width":0.00859375,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.58632815,"top":0.15972222,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.59765625,"top":0.15833333,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.60625,"top":0.15833333,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-661265807901631934
|
-5802339713239498621
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
activitySearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}...
|
NULL
|
|
11118
|
218
|
46
|
2026-04-14T09:13:51.706431+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158031706_m1.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
100+ matches in 26+ files
File mask: Find in Files
100+ matches in 26+ files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
sequence...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"role_description":"text"},{"role":"AXStaticText","text":"100+ matches in 26+ files","depth":1,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"sequence","depth":2,"value":"sequence","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
4679965429740400895
|
-8952663104527760037
|
click
|
accessibility
|
NULL
|
Find in Files
100+ matches in 26+ files
File mask: Find in Files
100+ matches in 26+ files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
sequence...
|
NULL
|
|
11119
|
219
|
50
|
2026-04-14T09:13:51.741141+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158031741_m2.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
100+ matches in 26+ files
File mask: Find in Files
100+ matches in 26+ files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
sequence...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"bounds":{"left":0.30039063,"top":0.12291667,"width":0.02890625,"height":0.011805556},"role_description":"text"},{"role":"AXStaticText","text":"100+ matches in 26+ files","depth":1,"bounds":{"left":0.33398438,"top":0.12291667,"width":0.062109374,"height":0.011805556},"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"bounds":{"left":0.5167969,"top":0.12013889,"width":0.034765624,"height":0.017361112},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"bounds":{"left":0.5527344,"top":0.11736111,"width":0.0328125,"height":0.023611112},"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"bounds":{"left":0.55742186,"top":0.12291667,"width":0.013671875,"height":0.011805556},"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"bounds":{"left":0.590625,"top":0.12013889,"width":0.00859375,"height":0.015277778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"bounds":{"left":0.6015625,"top":0.12013889,"width":0.00859375,"height":0.015277778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"bounds":{"left":0.296875,"top":0.14722222,"width":0.00859375,"height":0.015277778},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"sequence","depth":2,"bounds":{"left":0.30976564,"top":0.14722222,"width":0.2511719,"height":0.015277778},"value":"sequence","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
4679965429740400895
|
-8952663104527760037
|
click
|
accessibility
|
NULL
|
Find in Files
100+ matches in 26+ files
File mask: Find in Files
100+ matches in 26+ files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
sequence...
|
11117
|
|
11121
|
218
|
48
|
2026-04-14T09:13:57.397791+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158037397_m1.jpg...
|
PhpStorm
|
faVsco.js – OnDemandV2Controller.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
activitySearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Replace Field","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"activitySearch","depth":4,"value":"activitySearch","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/2","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
-661265807901631934
|
-5802339713239498621
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
activitySearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}...
|
NULL
|
|
11123
|
219
|
52
|
2026-04-14T09:13:57.880913+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158037880_m2.jpg...
|
PhpStorm
|
faVsco.js – OnDemandV2Controller.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
activitySearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Replace Field","depth":4,"bounds":{"left":0.30546874,"top":0.13472222,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.3203125,"top":0.13402778,"width":0.00859375,"height":0.015277778},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"activitySearch","depth":4,"bounds":{"left":0.33320314,"top":0.13402778,"width":0.0515625,"height":0.013888889},"value":"activitySearch","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.3953125,"top":0.13402778,"width":0.00859375,"height":0.015277778},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"bounds":{"left":0.40703124,"top":0.13402778,"width":0.00859375,"height":0.015277778},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"bounds":{"left":0.4171875,"top":0.13402778,"width":0.00859375,"height":0.015277778},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"bounds":{"left":0.42734376,"top":0.13402778,"width":0.00859375,"height":0.015277778},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/2","depth":4,"bounds":{"left":0.44335938,"top":0.13333334,"width":0.030078124,"height":0.015277778},"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"bounds":{"left":0.4734375,"top":0.13263889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"bounds":{"left":0.48359376,"top":0.13263889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"bounds":{"left":0.49375,"top":0.13263889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"bounds":{"left":0.50390625,"top":0.13263889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"bounds":{"left":0.5992187,"top":0.13263889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.57539064,"top":0.15972222,"width":0.00859375,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.58632815,"top":0.15972222,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.59765625,"top":0.15833333,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.60625,"top":0.15833333,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","depth":4,"bounds":{"left":0.30820313,"top":0.15694444,"width":0.38359374,"height":0.84305555},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Http\\Controllers\\API\\V2;\n\nuse Exception;\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\ValidationException;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Component\\AskAnything\\HistoryService;\nuse Jiminny\\Component\\AskJiminnyAi\\Exceptions\\AskJiminnyException;\nuse Jiminny\\Component\\AskJiminnyAi\\OnDemandLevel\\Events\\AskAnythingAbortedChatCompleted;\nuse Jiminny\\Component\\Prophet\\ProphetService;\nuse Jiminny\\Component\\ProphetAi\\StreamRequest;\nuse Jiminny\\Events\\EventDispatcher;\nuse Jiminny\\Exceptions\\ActivityProviderException;\nuse Jiminny\\Http\\Requests\\API\\V2\\OnDemandAskAnythingRequest;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ActiveStreamsRepository;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Repositories\\TeamAiContextRepository;\nuse Jiminny\\Utils\\FilterNormalizer;\nuse Jiminny\\VO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpFoundation\\Response as ResponseAlias;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\n\nclass OnDemandV2Controller extends Controller\n{\n use AuthorizesRequests;\n\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array FILTER_KEY_EXCLUDED_PARAMS = [\n 'sequence_number',\n 'page',\n 'per_page',\n 'limit',\n 'offset',\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly HistoryService $historyService,\n private readonly ProphetService $prophetService,\n private readonly TeamAiContextRepository $teamAiContextRepository,\n private readonly ActiveStreamsRepository $activeStreamsRepository,\n private readonly EventDispatcher $eventDispatcher,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled\n */\n private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse\n {\n if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {\n return new JsonResponse([\n 'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',\n ], ResponseAlias::HTTP_FORBIDDEN);\n }\n\n return null;\n }\n\n /**\n * Get top N activity IDs for Ask Jiminny feature based on filters\n *\n * @throws ValidationException\n * @throws ActivityProviderException\n */\n public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n $topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);\n\n try {\n // Always fetch first N (top count) IDs\n $onDemandActivitySearchCriteria = VO\\Repository\\OnDemandActivitySearch\\Criteria::createFromRequest(\n array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);\n\n $validationRules = $filterSet->getValidationRules()\n ->merge([\n 'exclude' => 'array',\n 'limit' => 'integer|min:1|max:' . $topCount,\n ])\n ->all();\n\n $request->validate($validationRules);\n\n $hasChangedFilters = $this->hasChangedContextFilter($request, $user);\n $activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);\n $this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);\n\n return new JsonResponse([\n 'count' => count($activityIds),\n 'changed_context_filters' => $hasChangedFilters,\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'ids' => [],\n 'error' => 'Failed to fetch activity IDs',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n private function hasChangedContextFilter(Request $request, User $user): bool\n {\n $filterKey = $this->makeFilterKey($request);\n\n $result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);\n\n if (! $result['changed']) {\n return false;\n }\n\n if ($result['matches_previous']) {\n return false;\n }\n\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n // If no history or last event already matches, return false\n if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {\n return false;\n }\n\n // Append event and notify\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_FILTERS_CHANGED_TYPE\n );\n\n return true;\n }\n\n private function makeFilterKey(Request $request): string\n {\n $filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);\n\n try {\n $normalizedFilters = FilterNormalizer::normalizeFilters($filters);\n $json = json_encode(\n $normalizedFilters,\n JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR\n );\n\n return hash('xxh3', $json);\n } catch (\\JsonException $e) {\n $this->logger->error('Failed to encode filters', [\n 'error' => $e->getMessage(),\n 'filters_keys' => array_keys($filters),\n ]);\n\n throw new AskJiminnyException('Failed to create filter key', 0, $e);\n }\n }\n\n\n /**\n * Get Ask Anything conversation history\n */\n public function getAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse($history, ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to fetch Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'history' => [],\n 'error' => 'Failed to fetch history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Delete Ask Anything conversation history\n */\n public function deleteAskAnythingHistory(Request $request): JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);\n\n return new JsonResponse([\n 'message' => 'History deleted successfully',\n ], ResponseAlias::HTTP_OK);\n } catch (Exception $e) {\n $this->logger->error('Failed to delete Ask Anything history', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to delete history',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n /**\n * Ask Anything - submit question and get AI response\n */\n public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse\n {\n /** @var User $user */\n $user = $request->user();\n\n $this->logger->info('AskAnything request received', [\n 'user_id' => $user->getId(),\n 'team_id' => $user->getTeamId(),\n 'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),\n 'message_preview' => mb_substr($request->input('message'), 0, 50),\n ]);\n\n if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {\n return $featureCheck;\n }\n\n try {\n $inputData = $request->validated();\n\n $requestData = [\n 'userQuestion' => $inputData['message'],\n 'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),\n 'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),\n ];\n\n $teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());\n if ($teamAiContext?->getPrompt() !== null) {\n $requestData['teamAiContext'] = $teamAiContext->getPrompt();\n }\n\n $this->historyService->appendToUserHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $inputData['message']\n );\n\n $this->historyService->appendToEventHistory(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n type: HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $streamRequest = StreamRequest::onDemandLevel($requestData);\n\n // Track active stream in Redis\n $this->activeStreamsRepository->start(\n $user->getId(),\n $streamRequest->getId(),\n ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()\n );\n\n return $this->prophetService->getStreamedOrAbortedResponse(\n streamRequest: $streamRequest,\n onCompleted: function (\n string $assistantResponse,\n bool $clientAborted,\n bool $hadError,\n ) use ($user, $streamRequest) {\n // Remove in-progress event\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n // Save to history if backend completed successfully (even if client disconnected)\n // Only skip saving if there was an actual error during streaming\n if (! $hadError && ! empty(trim($assistantResponse))) {\n $this->historyService->appendToSystemHistory(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n $assistantResponse,\n );\n }\n\n // Notify frontend if client disconnected (so it can update UI if still on page)\n if ($clientAborted) {\n $this->eventDispatcher->dispatch(\n new AskAnythingAbortedChatCompleted(\n user: $user,\n identifier: HistoryService::ON_DEMAND_SERVICE,\n response: $assistantResponse,\n )\n );\n }\n\n // Remove active stream in Redis\n $this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());\n }\n );\n } catch (Exception $e) {\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n $this->logger->error('Failed to process Ask Anything request', [\n 'error' => $e->getMessage(),\n 'user_id' => $user->getId(),\n 'question' => $request->input('message'),\n ]);\n\n return new JsonResponse([\n 'error' => 'Failed to process your question',\n ], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);\n }\n }\n\n public function stopStream(Request $request): JsonResponse\n {\n $user = $request->user();\n\n $this->activeStreamsRepository->stopByUser($user->getId());\n\n $this->historyService->removeLastEventMessageByType(\n $user,\n HistoryService::ON_DEMAND_SERVICE,\n HistoryService::EVENT_IN_PROGRESS_TYPE\n );\n\n return response()->json(['message' => 'Stream marked as stopped by user']);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.2589844,"top":0.28125,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.27265626,"top":0.28125,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.28398436,"top":0.27986112,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.29257813,"top":0.27986112,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
2033549508898738106
|
-5824280404149966240
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Show Replace Field
Search History
activitySearch
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/2
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
1
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Http\Controllers\API\V2;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Component\AskAnything\HistoryService;
use Jiminny\Component\AskJiminnyAi\Exceptions\AskJiminnyException;
use Jiminny\Component\AskJiminnyAi\OnDemandLevel\Events\AskAnythingAbortedChatCompleted;
use Jiminny\Component\Prophet\ProphetService;
use Jiminny\Component\ProphetAi\StreamRequest;
use Jiminny\Events\EventDispatcher;
use Jiminny\Exceptions\ActivityProviderException;
use Jiminny\Http\Requests\API\V2\OnDemandAskAnythingRequest;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\User;
use Jiminny\Repositories\ActiveStreamsRepository;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Repositories\TeamAiContextRepository;
use Jiminny\Utils\FilterNormalizer;
use Jiminny\VO;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
use Symfony\Component\HttpFoundation\StreamedResponse;
class OnDemandV2Controller extends Controller
{
use AuthorizesRequests;
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array FILTER_KEY_EXCLUDED_PARAMS = [
'sequence_number',
'page',
'per_page',
'limit',
'offset',
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly HistoryService $historyService,
private readonly ProphetService $prophetService,
private readonly TeamAiContextRepository $teamAiContextRepository,
private readonly ActiveStreamsRepository $activeStreamsRepository,
private readonly EventDispatcher $eventDispatcher,
private readonly LoggerInterface $logger,
) {
}
/**
* Check if the user's team has the ASK_JIMINNY_ON_ANYTHING feature enabled
*/
private function checkAskJiminnyOnAnythingFeature(User $user): ?JsonResponse
{
if (! $user->team->hasFeature(FeatureEnum::ASK_JIMINNY_ON_ANYTHING)) {
return new JsonResponse([
'message' => 'Feature ASK_JIMINNY_ON_ANYTHING is not enabled for this team',
], ResponseAlias::HTTP_FORBIDDEN);
}
return null;
}
/**
* Get top N activity IDs for Ask Jiminny feature based on filters
*
* @throws ValidationException
* @throws ActivityProviderException
*/
public function getContextForAskAnythingByFilter(Request $request, ElasticActivityRepository $repository): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
$topCount = $request->input('top_count', self::DEFAULT_TOP_ACTIVITIES_COUNT);
try {
// Always fetch first N (top count) IDs
$onDemandActivitySearchCriteria = VO\Repository\OnDemandActivitySearch\Criteria::createFromRequest(
array_merge($request->all(), ['limit' => $topCount, 'page' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($onDemandActivitySearchCriteria, $user);
$validationRules = $filterSet->getValidationRules()
->merge([
'exclude' => 'array',
'limit' => 'integer|min:1|max:' . $topCount,
])
->all();
$request->validate($validationRules);
$hasChangedFilters = $this->hasChangedContextFilter($request, $user);
$activityIds = $repository->onDemandSearchIdsOnly($user, $onDemandActivitySearchCriteria, $filterSet);
$this->historyService->storeContextIds($user, HistoryService::CONTEXT_IDS, $activityIds);
return new JsonResponse([
'count' => count($activityIds),
'changed_context_filters' => $hasChangedFilters,
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch activity IDs for Ask Jiminny', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'ids' => [],
'error' => 'Failed to fetch activity IDs',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function hasChangedContextFilter(Request $request, User $user): bool
{
$filterKey = $this->makeFilterKey($request);
$result = $this->historyService->compareAndSetFilterKeyWithHistory($user, $filterKey);
if (! $result['changed']) {
return false;
}
if ($result['matches_previous']) {
return false;
}
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
// If no history or last event already matches, return false
if (empty($history) || $this->historyService->hasFilteredChangedEventAsLastMessage($history)) {
return false;
}
// Append event and notify
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_FILTERS_CHANGED_TYPE
);
return true;
}
private function makeFilterKey(Request $request): string
{
$filters = $request->except(self::FILTER_KEY_EXCLUDED_PARAMS);
try {
$normalizedFilters = FilterNormalizer::normalizeFilters($filters);
$json = json_encode(
$normalizedFilters,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
return hash('xxh3', $json);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode filters', [
'error' => $e->getMessage(),
'filters_keys' => array_keys($filters),
]);
throw new AskJiminnyException('Failed to create filter key', 0, $e);
}
}
/**
* Get Ask Anything conversation history
*/
public function getAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$history = $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse($history, ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to fetch Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'history' => [],
'error' => 'Failed to fetch history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete Ask Anything conversation history
*/
public function deleteAskAnythingHistory(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$this->historyService->deleteHistory($user, HistoryService::ON_DEMAND_SERVICE);
return new JsonResponse([
'message' => 'History deleted successfully',
], ResponseAlias::HTTP_OK);
} catch (Exception $e) {
$this->logger->error('Failed to delete Ask Anything history', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
]);
return new JsonResponse([
'error' => 'Failed to delete history',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Ask Anything - submit question and get AI response
*/
public function askAnything(OnDemandAskAnythingRequest $request): StreamedResponse|JsonResponse
{
/** @var User $user */
$user = $request->user();
$this->logger->info('AskAnything request received', [
'user_id' => $user->getId(),
'team_id' => $user->getTeamId(),
'request_hash' => md5($request->input('message') . $user->getId() . floor(time() / 60)),
'message_preview' => mb_substr($request->input('message'), 0, 50),
]);
if ($featureCheck = $this->checkAskJiminnyOnAnythingFeature($user)) {
return $featureCheck;
}
try {
$inputData = $request->validated();
$requestData = [
'userQuestion' => $inputData['message'],
'callIds' => $this->historyService->getContextIds($user, HistoryService::CONTEXT_IDS),
'history' => $this->historyService->getHistory($user, HistoryService::ON_DEMAND_SERVICE),
];
$teamAiContext = $this->teamAiContextRepository->getByTeamId($user->getTeamId());
if ($teamAiContext?->getPrompt() !== null) {
$requestData['teamAiContext'] = $teamAiContext->getPrompt();
}
$this->historyService->appendToUserHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$inputData['message']
);
$this->historyService->appendToEventHistory(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
type: HistoryService::EVENT_IN_PROGRESS_TYPE
);
$streamRequest = StreamRequest::onDemandLevel($requestData);
// Track active stream in Redis
$this->activeStreamsRepository->start(
$user->getId(),
$streamRequest->getId(),
ttlSeconds: $streamRequest->getConnectTimeout() + $streamRequest->getReadTimeout()
);
return $this->prophetService->getStreamedOrAbortedResponse(
streamRequest: $streamRequest,
onCompleted: function (
string $assistantResponse,
bool $clientAborted,
bool $hadError,
) use ($user, $streamRequest) {
// Remove in-progress event
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
// Save to history if backend completed successfully (even if client disconnected)
// Only skip saving if there was an actual error during streaming
if (! $hadError && ! empty(trim($assistantResponse))) {
$this->historyService->appendToSystemHistory(
$user,
HistoryService::ON_DEMAND_SERVICE,
$assistantResponse,
);
}
// Notify frontend if client disconnected (so it can update UI if still on page)
if ($clientAborted) {
$this->eventDispatcher->dispatch(
new AskAnythingAbortedChatCompleted(
user: $user,
identifier: HistoryService::ON_DEMAND_SERVICE,
response: $assistantResponse,
)
);
}
// Remove active stream in Redis
$this->activeStreamsRepository->stop($user->getId(), $streamRequest->getId());
}
);
} catch (Exception $e) {
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
$this->logger->error('Failed to process Ask Anything request', [
'error' => $e->getMessage(),
'user_id' => $user->getId(),
'question' => $request->input('message'),
]);
return new JsonResponse([
'error' => 'Failed to process your question',
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function stopStream(Request $request): JsonResponse
{
$user = $request->user();
$this->activeStreamsRepository->stopByUser($user->getId());
$this->historyService->removeLastEventMessageByType(
$user,
HistoryService::ON_DEMAND_SERVICE,
HistoryService::EVENT_IN_PROGRESS_TYPE
);
return response()->json(['message' => 'Stream marked as stopped by user']);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11122
|
|
11138
|
221
|
4
|
2026-04-14T09:14:41.170197+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158081170_m2.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.009375,"height":0.0},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.008203125,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.2589844,"top":0.28125,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.27265626,"top":0.28125,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.28398436,"top":0.27986112,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.29257813,"top":0.27986112,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3342537313573719871
|
-5967358222366069132
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All...
|
11137
|
|
11144
|
221
|
7
|
2026-04-14T09:14:49.104776+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158089104_m2.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.009375,"height":0.0},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.008203125,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.2589844,"top":0.28125,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.27265626,"top":0.28125,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.28398436,"top":0.27986112,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.29257813,"top":0.27986112,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8563436494813991353
|
-5967358188006330764
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
11152
|
220
|
10
|
2026-04-14T09:15:32.482673+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158132482_m1.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8563436494813991353
|
-5967358188006330764
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
11153
|
221
|
12
|
2026-04-14T09:15:39.834484+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158139834_m2.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.009375,"height":0.0},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.008203125,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.2589844,"top":0.28125,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.27265626,"top":0.28125,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.28398436,"top":0.27986112,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.29257813,"top":0.27986112,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8563436494813991353
|
-5967358188006330764
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11151
|
|
11154
|
220
|
11
|
2026-04-14T09:16:02.706546+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158162706_m1.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8563436494813991353
|
-5967358188006330764
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11152
|
|
11155
|
221
|
13
|
2026-04-14T09:16:10.155109+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158170155_m2.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.009375,"height":0.0},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.008203125,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.2589844,"top":0.28125,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.27265626,"top":0.28125,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.28398436,"top":0.27986112,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.29257813,"top":0.27986112,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8563436494813991353
|
-5967358188006330764
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
11156
|
220
|
12
|
2026-04-14T09:16:32.963324+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158192963_m1.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8563436494813991353
|
-5967358188006330764
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11152
|
|
11157
|
221
|
14
|
2026-04-14T09:16:40.454196+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158200454_m2.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.009375,"height":0.0},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.008203125,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.2589844,"top":0.28125,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.27265626,"top":0.28125,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.28398436,"top":0.27986112,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.29257813,"top":0.27986112,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8563436494813991353
|
-5967358188006330764
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11155
|
|
11158
|
220
|
13
|
2026-04-14T09:17:03.181772+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158223181_m1.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8563436494813991353
|
-5967358188006330764
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11152
|
|
11159
|
221
|
15
|
2026-04-14T09:17:10.786821+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158230786_m2.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.009375,"height":0.0},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.008203125,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.2589844,"top":0.28125,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.27265626,"top":0.28125,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.28398436,"top":0.27986112,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.29257813,"top":0.27986112,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8563436494813991353
|
-5967358188006330764
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11155
|
|
11163
|
220
|
15
|
2026-04-14T09:17:23.592325+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158243592_m1.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.02111111},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8563436494813991353
|
-5967358188006330764
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
11164
|
221
|
18
|
2026-04-14T09:17:23.632452+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158243632_m2.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.009375,"height":0.0},"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.00859375,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.008203125,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityService\n{\n private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;\n\n private const array DATE_FILTER_KEYS = [\n ActivityActualDate::PARAM_START_DATE,\n ActivityActualDate::PARAM_END_DATE,\n ActivityUpdatedDate::PARAM_UPDATED_FROM,\n ActivityUpdatedDate::PARAM_UPDATED_TO,\n ClosingPeriodFilter::KEY_START_DATE,\n ClosingPeriodFilter::KEY_END_DATE,\n ];\n\n public function __construct(\n private readonly ActivitySearch $activitySearch,\n private readonly ElasticActivityRepository $elasticRepository,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Fetch activity IDs for a saved search, passing its filters as-is to Criteria.\n * Date filters stored on the saved search are excluded; if no other filters exist,\n * no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.\n *\n * @return string[] Activity IDs\n */\n public function getActivityIdsForSavedSearch(\n Search $savedSearch,\n User $user,\n ): array {\n $requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);\n\n $criteria = Criteria::createFromRequest(\n array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),\n $user->getTimezone()\n );\n\n $filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);\n\n $activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);\n\n $this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [\n 'saved_search_id' => $savedSearch->getId(),\n 'user_id' => $user->getId(),\n 'activity_count' => count($activityIds),\n ]);\n\n return $activityIds;\n }\n\n private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array\n {\n $params = [];\n $arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);\n\n foreach ($savedSearch->getFilters() as $filter) {\n $key = $filter->getFilterProperty();\n $value = $filter->getFilterValue();\n\n if (in_array($key, self::DATE_FILTER_KEYS, true)) {\n continue;\n }\n\n if (isset($params[$key])) {\n $params[$key][] = $value;\n } elseif (in_array($key, $arrayFilterKeys, true)) {\n $params[$key] = [$value];\n } else {\n $params[$key] = $value;\n }\n }\n\n return $params;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.2589844,"top":0.28125,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.27265626,"top":0.28125,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.28398436,"top":0.27986112,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.29257813,"top":0.27986112,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Repositories;\n\nuse Carbon\\CarbonImmutable;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Pagination\\LengthAwarePaginator;\nuse Illuminate\\Support\\Facades\\DB;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\AutomatedReportResult;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSort;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\ReportSortDirection;\n\nclass AutomatedReportsRepository\n{\n /**\n * Create a new automated report\n *\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function create(array $data): AutomatedReport\n {\n return AutomatedReport::create($data);\n }\n\n /**\n * Find an automated report by UUID\n *\n * @param string $uuid\n *\n * @return AutomatedReport|null\n */\n public function findByUuid(string $uuid): ?AutomatedReport\n {\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();\n }\n\n public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport\n {\n if (is_numeric($idOrUuid)) {\n return AutomatedReport::find((int) $idOrUuid);\n }\n\n return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();\n }\n\n /**\n * Retrieve all standard (non-Ask Jiminny) automated reports.\n *\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAllStandardReports(\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->get();\n }\n\n /**\n * Retrieve all Ask Jiminny reports created by the given user.\n *\n * @param User $user The user whose reports to retrieve.\n * @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.\n * @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.\n *\n * @return Collection<AutomatedReport>\n */\n public function getAskJiminnyReportsByUser(\n User $user,\n string $sortColumn = 'created_at',\n string $sortDirection = 'desc'\n ): Collection {\n return $this->buildSortedQuery($sortColumn, $sortDirection)\n ->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)\n ->where('created_by', $user->getId())\n ->get();\n }\n\n private function buildSortedQuery(string $sortColumn, string $sortDirection): \\Illuminate\\Database\\Eloquent\\Builder\n {\n $allowedColumns = ['created_by', 'created_at'];\n if (! in_array($sortColumn, $allowedColumns)) {\n $sortColumn = 'created_at';\n }\n\n $sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';\n\n $query = AutomatedReport::query()->with(['creator', 'team']);\n\n if ($sortColumn === 'created_by') {\n $query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')\n ->orderByRaw(\"users.name COLLATE utf8mb4_unicode_ci {$sortDirection}\")\n ->select('automated_reports.*');\n } else {\n $query->orderBy($sortColumn, $sortDirection);\n }\n\n return $query;\n }\n\n /**\n * Get all active and enabled reports with active teams for the specified frequency.\n *\n * @param string $frequency\n *\n * @return Collection<AutomatedReport>\n */\n public function getActiveReportsByFrequency(string $frequency): Collection\n {\n return AutomatedReport::where('automated_reports.status', true)\n ->where('automated_reports.frequency', $frequency)\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->where('teams.status', Team::STATUS_ACTIVE)\n ->where(function ($query) {\n $query->whereNull('automated_reports.expires_at')\n ->orWhere('automated_reports.expires_at', '>=', now()->toDateString());\n })\n ->select('automated_reports.*')\n ->get();\n }\n\n /**\n * Update an automated report\n *\n * @param AutomatedReport $report\n * @param array $data\n *\n * @return AutomatedReport\n */\n public function update(AutomatedReport $report, array $data): AutomatedReport\n {\n $report->update($data);\n\n return $report;\n }\n\n /**\n * Create a new automated report result.\n *\n * @param array $data The data to create the automated report result with.\n *\n * @return AutomatedReportResult The newly created automated report result.\n */\n public function createResult(array $data): AutomatedReportResult\n {\n return AutomatedReportResult::create($data);\n }\n\n /**\n * Find an automated report result by UUID.\n *\n * @param string $uuid The UUID to find the automated report result with.\n *\n * @return AutomatedReportResult|null The automated report result if found, otherwise null.\n */\n public function findResultByUuid(string $uuid): ?AutomatedReportResult\n {\n return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();\n }\n\n public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('uuid', AutomatedReportResult::toOptimized($uuid))\n ->whereHas('report', static function ($query) use ($user): void {\n $query->where('team_id', $user->getTeamId())\n ->where('created_by', $user->getId());\n })\n ->first();\n }\n\n public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult\n {\n return AutomatedReportResult::query()\n ->where('parent_id', $result->getId())\n ->where('media_type', $type)\n ->first();\n }\n\n public function getGeneratedNotSentResults(): Collection\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNull('sent_at')\n ->where('status', AutomatedReportResult::STATUS_GENERATED)\n ->whereHas('report')\n ->with('report')\n ->get();\n }\n\n public function getPaginatedUserReports(\n User $user,\n ReportSort $sort,\n ReportSortDirection $sortDirection,\n int $resultsPerPage,\n int $page,\n ?Carbon $fromDate,\n ?Carbon $toDate,\n array $teamIds,\n array $reportTypes,\n ?string $name,\n ): LengthAwarePaginator {\n $query = AutomatedReportResult::query()\n ->whereNotNull('automated_report_results.generated_at')\n ->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')\n ->where('automated_reports.team_id', $user->getTeamId())\n ->whereJsonContains('automated_reports.recipients->users', $user->getId())\n ->orderByRaw(\"$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}\")\n ->select('automated_report_results.*')\n ->with('report.team');\n\n if ($fromDate !== null && $toDate !== null) {\n $query->whereBetween('generated_at', [$fromDate, $toDate]);\n }\n\n if (! empty($teamIds)) {\n $query->where(function ($q) use ($teamIds) {\n foreach ($teamIds as $id) {\n $q->orWhereJsonContains('automated_reports.groups', $id);\n }\n });\n }\n\n if (! empty($reportTypes)) {\n $query->whereIn('automated_reports.type', $reportTypes);\n }\n\n if (! empty($name)) {\n $query->whereLike('name', \"%$name%\");\n }\n\n return $query->paginate($resultsPerPage, ['*'], 'page', $page);\n }\n\n public function countUserReports(User $user): int\n {\n return AutomatedReportResult::query()\n ->whereNotNull('generated_at')\n ->whereNotNull('sent_at')\n ->whereHas('report', function ($q) use ($user) {\n $q->where('team_id', $user->getTeamId())\n ->whereJsonContains('recipients->users', $user->getId());\n })\n ->count();\n }\n\n /**\n * Get report IDs for a specific team\n *\n * @param Team $team\n *\n * @return \\Illuminate\\Support\\Collection\n */\n public function getReportIdsByTeam(Team $team): \\Illuminate\\Support\\Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->pluck('id');\n }\n\n /**\n * Get all reports for a specific team\n *\n * @param Team $team\n *\n * @return Collection\n */\n public function getReportsByTeam(Team $team): Collection\n {\n return AutomatedReport::where('team_id', $team->getId())->get();\n }\n\n /**\n * Get all report results for a specific report\n *\n * @param AutomatedReport $report\n *\n * @return Collection\n */\n public function getResultsByReport(AutomatedReport $report): Collection\n {\n return $this->getResultsByReportQuery($report)->get();\n }\n\n public function getResultsByReportQuery(AutomatedReport $report): Builder\n {\n return AutomatedReportResult::where('report_id', $report->getId());\n }\n\n public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder\n {\n $reportIds = $this->getReportIdsByTeam($team);\n\n return AutomatedReportResult::query()->whereIn('report_id', $reportIds)\n ->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);\n }\n\n /**\n * @param int|null $teamId Optional team ID to filter results\n *\n * @return \\Illuminate\\Support\\Collection<int, int> Collection of team IDs\n */\n public function getTeamIdsWithReportsResults(?int $teamId = null): \\Illuminate\\Support\\Collection\n {\n $query = DB::table('automated_reports')\n ->join('teams', 'automated_reports.team_id', '=', 'teams.id')\n ->select('teams.id')\n ->distinct();\n\n if ($teamId !== null) {\n $query->where('teams.id', $teamId);\n }\n\n return $query->pluck('teams.id');\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8563436494813991353
|
-5967358188006330764
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
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\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityService
{
private const int DEFAULT_TOP_ACTIVITIES_COUNT = 100;
private const array DATE_FILTER_KEYS = [
ActivityActualDate::PARAM_START_DATE,
ActivityActualDate::PARAM_END_DATE,
ActivityUpdatedDate::PARAM_UPDATED_FROM,
ActivityUpdatedDate::PARAM_UPDATED_TO,
ClosingPeriodFilter::KEY_START_DATE,
ClosingPeriodFilter::KEY_END_DATE,
];
public function __construct(
private readonly ActivitySearch $activitySearch,
private readonly ElasticActivityRepository $elasticRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Fetch activity IDs for a saved search, passing its filters as-is to Criteria.
* Date filters stored on the saved search are excluded; if no other filters exist,
* no date constraint is applied — matching the behaviour of getContextForAskAnythingByFilter.
*
* @return string[] Activity IDs
*/
public function getActivityIdsForSavedSearch(
Search $savedSearch,
User $user,
): array {
$requestParams = $this->buildRequestParamsFromSearch($savedSearch, $user);
$criteria = Criteria::createFromRequest(
array_merge($requestParams, ['limit' => self::DEFAULT_TOP_ACTIVITIES_COUNT, 'page' => 1, 'sequence_number' => 1]),
$user->getTimezone()
);
$filterSet = $this->activitySearch->getOnDemandPageFilterSet($criteria, $user);
$activityIds = $this->elasticRepository->onDemandSearchIdsOnly($user, $criteria, $filterSet);
$this->logger->info('[AskJiminnyReport] Fetched activity IDs for saved search', [
'saved_search_id' => $savedSearch->getId(),
'user_id' => $user->getId(),
'activity_count' => count($activityIds),
]);
return $activityIds;
}
private function buildRequestParamsFromSearch(Search $savedSearch, User $user): array
{
$params = [];
$arrayFilterKeys = $this->activitySearch->getArrayFilterKeys($user);
foreach ($savedSearch->getFilters() as $filter) {
$key = $filter->getFilterProperty();
$value = $filter->getFilterValue();
if (in_array($key, self::DATE_FILTER_KEYS, true)) {
continue;
}
if (isset($params[$key])) {
$params[$key][] = $value;
} elseif (in_array($key, $arrayFilterKeys, true)) {
$params[$key] = [$value];
} else {
$params[$key] = $value;
}
}
return $params;
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Repositories;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Jiminny\Models\AutomatedReport;
use Jiminny\Models\AutomatedReportResult;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\Kiosk\AutomatedReports\AutomatedReportsService;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSort;
use Jiminny\Services\Kiosk\AutomatedReports\ReportSortDirection;
class AutomatedReportsRepository
{
/**
* Create a new automated report
*
* @param array $data
*
* @return AutomatedReport
*/
public function create(array $data): AutomatedReport
{
return AutomatedReport::create($data);
}
/**
* Find an automated report by UUID
*
* @param string $uuid
*
* @return AutomatedReport|null
*/
public function findByUuid(string $uuid): ?AutomatedReport
{
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($uuid))->first();
}
public function findByIdOrUuid(string $idOrUuid): ?AutomatedReport
{
if (is_numeric($idOrUuid)) {
return AutomatedReport::find((int) $idOrUuid);
}
return AutomatedReport::where('uuid', AutomatedReport::toOptimized($idOrUuid))->first();
}
/**
* Retrieve all standard (non-Ask Jiminny) automated reports.
*
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAllStandardReports(
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->whereNot('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->get();
}
/**
* Retrieve all Ask Jiminny reports created by the given user.
*
* @param User $user The user whose reports to retrieve.
* @param string $sortColumn The column to sort by. Allowed values: 'created_by', 'created_at'. Defaults to 'created_at'.
* @param string $sortDirection The sort direction. Allowed values: 'asc', 'desc'. Defaults to 'desc'.
*
* @return Collection<AutomatedReport>
*/
public function getAskJiminnyReportsByUser(
User $user,
string $sortColumn = 'created_at',
string $sortDirection = 'desc'
): Collection {
return $this->buildSortedQuery($sortColumn, $sortDirection)
->where('type', AutomatedReportsService::TYPE_ASK_JIMINNY)
->where('created_by', $user->getId())
->get();
}
private function buildSortedQuery(string $sortColumn, string $sortDirection): \Illuminate\Database\Eloquent\Builder
{
$allowedColumns = ['created_by', 'created_at'];
if (! in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'created_at';
}
$sortDirection = strtolower($sortDirection) === 'asc' ? 'asc' : 'desc';
$query = AutomatedReport::query()->with(['creator', 'team']);
if ($sortColumn === 'created_by') {
$query->leftJoin('users', 'users.id', '=', 'automated_reports.created_by')
->orderByRaw("users.name COLLATE utf8mb4_unicode_ci {$sortDirection}")
->select('automated_reports.*');
} else {
$query->orderBy($sortColumn, $sortDirection);
}
return $query;
}
/**
* Get all active and enabled reports with active teams for the specified frequency.
*
* @param string $frequency
*
* @return Collection<AutomatedReport>
*/
public function getActiveReportsByFrequency(string $frequency): Collection
{
return AutomatedReport::where('automated_reports.status', true)
->where('automated_reports.frequency', $frequency)
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->where('teams.status', Team::STATUS_ACTIVE)
->where(function ($query) {
$query->whereNull('automated_reports.expires_at')
->orWhere('automated_reports.expires_at', '>=', now()->toDateString());
})
->select('automated_reports.*')
->get();
}
/**
* Update an automated report
*
* @param AutomatedReport $report
* @param array $data
*
* @return AutomatedReport
*/
public function update(AutomatedReport $report, array $data): AutomatedReport
{
$report->update($data);
return $report;
}
/**
* Create a new automated report result.
*
* @param array $data The data to create the automated report result with.
*
* @return AutomatedReportResult The newly created automated report result.
*/
public function createResult(array $data): AutomatedReportResult
{
return AutomatedReportResult::create($data);
}
/**
* Find an automated report result by UUID.
*
* @param string $uuid The UUID to find the automated report result with.
*
* @return AutomatedReportResult|null The automated report result if found, otherwise null.
*/
public function findResultByUuid(string $uuid): ?AutomatedReportResult
{
return AutomatedReportResult::where('uuid', AutomatedReportResult::toOptimized($uuid))->first();
}
public function findResultByUuidForUser(string $uuid, User $user): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('uuid', AutomatedReportResult::toOptimized($uuid))
->whereHas('report', static function ($query) use ($user): void {
$query->where('team_id', $user->getTeamId())
->where('created_by', $user->getId());
})
->first();
}
public function findChildResult(AutomatedReportResult $result, string $type): ?AutomatedReportResult
{
return AutomatedReportResult::query()
->where('parent_id', $result->getId())
->where('media_type', $type)
->first();
}
public function getGeneratedNotSentResults(): Collection
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNull('sent_at')
->where('status', AutomatedReportResult::STATUS_GENERATED)
->whereHas('report')
->with('report')
->get();
}
public function getPaginatedUserReports(
User $user,
ReportSort $sort,
ReportSortDirection $sortDirection,
int $resultsPerPage,
int $page,
?Carbon $fromDate,
?Carbon $toDate,
array $teamIds,
array $reportTypes,
?string $name,
): LengthAwarePaginator {
$query = AutomatedReportResult::query()
->whereNotNull('automated_report_results.generated_at')
->join('automated_reports', 'automated_report_results.report_id', '=', 'automated_reports.id')
->where('automated_reports.team_id', $user->getTeamId())
->whereJsonContains('automated_reports.recipients->users', $user->getId())
->orderByRaw("$sort->value COLLATE utf8mb4_unicode_ci {$sortDirection->value}")
->select('automated_report_results.*')
->with('report.team');
if ($fromDate !== null && $toDate !== null) {
$query->whereBetween('generated_at', [$fromDate, $toDate]);
}
if (! empty($teamIds)) {
$query->where(function ($q) use ($teamIds) {
foreach ($teamIds as $id) {
$q->orWhereJsonContains('automated_reports.groups', $id);
}
});
}
if (! empty($reportTypes)) {
$query->whereIn('automated_reports.type', $reportTypes);
}
if (! empty($name)) {
$query->whereLike('name', "%$name%");
}
return $query->paginate($resultsPerPage, ['*'], 'page', $page);
}
public function countUserReports(User $user): int
{
return AutomatedReportResult::query()
->whereNotNull('generated_at')
->whereNotNull('sent_at')
->whereHas('report', function ($q) use ($user) {
$q->where('team_id', $user->getTeamId())
->whereJsonContains('recipients->users', $user->getId());
})
->count();
}
/**
* Get report IDs for a specific team
*
* @param Team $team
*
* @return \Illuminate\Support\Collection
*/
public function getReportIdsByTeam(Team $team): \Illuminate\Support\Collection
{
return AutomatedReport::where('team_id', $team->getId())->pluck('id');
}
/**
* Get all reports for a specific team
*
* @param Team $team
*
* @return Collection
*/
public function getReportsByTeam(Team $team): Collection
{
return AutomatedReport::where('team_id', $team->getId())->get();
}
/**
* Get all report results for a specific report
*
* @param AutomatedReport $report
*
* @return Collection
*/
public function getResultsByReport(AutomatedReport $report): Collection
{
return $this->getResultsByReportQuery($report)->get();
}
public function getResultsByReportQuery(AutomatedReport $report): Builder
{
return AutomatedReportResult::where('report_id', $report->getId());
}
public function getReportResultsQueryForRetention(Team $team, CarbonImmutable $retentionDate): Builder
{
$reportIds = $this->getReportIdsByTeam($team);
return AutomatedReportResult::query()->whereIn('report_id', $reportIds)
->whereRaw('IFNULL(generated_at, created_at) <= ?', [$retentionDate]);
}
/**
* @param int|null $teamId Optional team ID to filter results
*
* @return \Illuminate\Support\Collection<int, int> Collection of team IDs
*/
public function getTeamIdsWithReportsResults(?int $teamId = null): \Illuminate\Support\Collection
{
$query = DB::table('automated_reports')
->join('teams', 'automated_reports.team_id', '=', 'teams.id')
->select('teams.id')
->distinct();
if ($teamId !== null) {
$query->where('teams.id', $teamId);
}
return $query->pluck('teams.id');
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11162
|
|
11183
|
220
|
25
|
2026-04-14T09:18:16.726564+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158296726_m1.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityServiceTest.ph faVsco.js – AskJiminnyReportActivityServiceTest.php...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
409850090631519174
|
938142628803978660
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error...
|
NULL
|
|
11184
|
221
|
28
|
2026-04-14T09:18:16.688972+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158296688_m2.jpg...
|
PhpStorm
|
faVsco.js – AskJiminnyReportActivityServiceTest.ph faVsco.js – AskJiminnyReportActivityServiceTest.php...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.575,"top":0.13055556,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.58671874,"top":0.13055556,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5980469,"top":0.12916666,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.60664064,"top":0.12916666,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","depth":4,"bounds":{"left":0.32617188,"top":0.0,"width":0.3347656,"height":1.0},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.2589844,"top":0.28125,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.27265626,"top":0.28125,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.28398436,"top":0.27986112,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.29257813,"top":0.27986112,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
409850090631519174
|
938142628803978660
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
15
4
Previous Highlighted Error
Next Highlighted Error...
|
11182
|
|
11195
|
220
|
31
|
2026-04-14T09:18:55.441603+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158335441_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
2353231120370746233
|
2960206221971779892
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
11196
|
221
|
34
|
2026-04-14T09:18:55.441547+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158335441_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.575,"top":0.19513889,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.58671874,"top":0.19513889,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5980469,"top":0.19375,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.60664064,"top":0.19375,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","depth":4,"bounds":{"left":0.4296875,"top":0.0,"width":0.3347656,"height":1.0},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.3761719,"top":0.19513889,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.3875,"top":0.19375,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.39609376,"top":0.19375,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
2353231120370746233
|
2960206221971779892
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11194
|
|
11203
|
222
|
1
|
2026-04-14T09:19:44.644452+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158384644_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports \n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). \n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports \n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). \n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5719932180597039997
|
2960206221971779892
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
|
11204
|
223
|
1
|
2026-04-14T09:19:44.644465+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158384644_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.575,"top":0.19513889,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.58671874,"top":0.19513889,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5980469,"top":0.19375,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.60664064,"top":0.19375,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","depth":4,"bounds":{"left":0.32890624,"top":0.0,"width":0.3347656,"height":1.0},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.3761719,"top":0.19513889,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.3875,"top":0.19375,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.39609376,"top":0.19375,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports \n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). \n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","depth":4,"bounds":{"left":0.09609375,"top":0.00069444446,"width":0.346875,"height":0.99930555},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports \n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). \n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5719932180597039997
|
2960206221971779892
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11202
|
|
11207
|
222
|
3
|
2026-04-14T09:19:50.286719+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158390286_m1.jpg...
|
Firefox
|
Jiminny — Work
|
1
|
app.staging.jiminny.com/ondemand?topic_id[]=e02f09 app.staging.jiminny.com/ondemand?topic_id[]=e02f0932-cb76-41b6-ac4f-6b8db1392146&include_internal_conversations=1&sequence_number=4...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
JY-20543 add AJ reports User pilot tracking by Lak JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
Platform Sprint 1 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 1 Q2 - Platform Team - Scrum Board - Jira
JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
Configure SSH access to multiple environment - Engineering - Confluence
Configure SSH access to multiple environment - Engineering - Confluence
Console Home | Console Home | us-east-2
Console Home | Console Home | us-east-2
SecurityGroup | EC2 | us-east-2
SecurityGroup | EC2 | us-east-2
JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
SRD-6779 | JY-20632 | Unable to log in to Sidekick with SSO by yalokin-jiminny · Pull Request #11935 · jiminny/app
SRD-6779 | JY-20632 | Unable to log in to Sidekick with SSO by yalokin-jiminny · Pull Request #11935 · jiminny/app
Jy 19798 evaluation for ai activity types by nikolaybiaivanov · Pull Request #468 · jiminny/prophet
Jy 19798 evaluation for ai activity types by nikolaybiaivanov · Pull Request #468 · jiminny/prophet
Jiminny
Jiminny...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Platform Sprint 1 Q2 - Platform Team - Scrum Board - Jira","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Sprint 1 Q2 - Platform Team - Scrum Board - Jira","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Configure SSH access to multiple environment - Engineering - Confluence","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Configure SSH access to multiple environment - Engineering - Confluence","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Console Home | Console Home | us-east-2","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Console Home | Console Home | us-east-2","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SecurityGroup | EC2 | us-east-2","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SecurityGroup | EC2 | us-east-2","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SRD-6779 | JY-20632 | Unable to log in to Sidekick with SSO by yalokin-jiminny · Pull Request #11935 · jiminny/app","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SRD-6779 | JY-20632 | Unable to log in to Sidekick with SSO by yalokin-jiminny · Pull Request #11935 · jiminny/app","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jy 19798 evaluation for ai activity types by nikolaybiaivanov · Pull Request #468 · jiminny/prophet","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 19798 evaluation for ai activity types by nikolaybiaivanov · Pull Request #468 · jiminny/prophet","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Jiminny","depth":5,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-3499077385187610281
|
1521462278137570926
|
visual_change
|
accessibility
|
NULL
|
JY-20543 add AJ reports User pilot tracking by Lak JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
Platform Sprint 1 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 1 Q2 - Platform Team - Scrum Board - Jira
JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
Configure SSH access to multiple environment - Engineering - Confluence
Configure SSH access to multiple environment - Engineering - Confluence
Console Home | Console Home | us-east-2
Console Home | Console Home | us-east-2
SecurityGroup | EC2 | us-east-2
SecurityGroup | EC2 | us-east-2
JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
SRD-6779 | JY-20632 | Unable to log in to Sidekick with SSO by yalokin-jiminny · Pull Request #11935 · jiminny/app
SRD-6779 | JY-20632 | Unable to log in to Sidekick with SSO by yalokin-jiminny · Pull Request #11935 · jiminny/app
Jy 19798 evaluation for ai activity types by nikolaybiaivanov · Pull Request #468 · jiminny/prophet
Jy 19798 evaluation for ai activity types by nikolaybiaivanov · Pull Request #468 · jiminny/prophet
Jiminny
Jiminny...
|
NULL
|
|
11208
|
223
|
3
|
2026-04-14T09:19:50.711510+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158390711_m2.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.575,"top":0.19513889,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.58671874,"top":0.19513889,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5980469,"top":0.19375,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.60664064,"top":0.19375,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","depth":4,"bounds":{"left":0.32890624,"top":0.0,"width":0.3347656,"height":1.0},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.3761719,"top":0.19513889,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.3875,"top":0.19375,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.39609376,"top":0.19375,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports \n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). \n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","depth":4,"bounds":{"left":0.09609375,"top":0.00069444446,"width":0.346875,"height":0.99930555},"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports \n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). \n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"}]...
|
2309070296108418966
|
2960206084532827444
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project...
|
11206
|
|
11209
|
222
|
4
|
2026-04-14T09:19:53.828798+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158393828_m1.jpg...
|
PhpStorm
|
faVsco.js – AutomatedReportsCommand.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Kiosk\\AutomatedReports;\n\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityActualDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\ActivityUpdatedDate;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition\\DealInsights\\ClosingPeriodFilter;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionCollection;\nuse Jiminny\\Component\\ActivitySearch\\Service\\ActivitySearch;\nuse Jiminny\\Models\\Activity\\Search;\nuse Jiminny\\Models\\Activity\\SearchFilter;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\ElasticActivityRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AskJiminnyReportActivityService;\nuse Jiminny\\VO\\Repository\\OnDemandActivitySearch\\Criteria;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nclass AskJiminnyReportActivityServiceTest extends TestCase\n{\n private ActivitySearch&MockObject $activitySearch;\n private ElasticActivityRepository&MockObject $elasticRepository;\n private LoggerInterface&MockObject $logger;\n private AskJiminnyReportActivityService $service;\n\n protected function setUp(): void\n {\n $this->activitySearch = $this->createMock(ActivitySearch::class);\n $this->elasticRepository = $this->createMock(ElasticActivityRepository::class);\n $this->logger = $this->createMock(LoggerInterface::class);\n\n $this->service = new AskJiminnyReportActivityService(\n $this->activitySearch,\n $this->elasticRepository,\n $this->logger,\n );\n }\n\n private function makeFilter(string $key, ?string $value): SearchFilter&MockObject\n {\n $filter = $this->createMock(SearchFilter::class);\n $filter->method('getFilterProperty')->willReturn($key);\n $filter->method('getFilterValue')->willReturn($value);\n\n return $filter;\n }\n\n private function makeUser(): User&MockObject\n {\n $tz = new \\DateTimeZone('UTC');\n $user = $this->createMock(User::class);\n $user->method('getTimezone')->willReturn($tz);\n $user->method('getId')->willReturn(1);\n $user->method('getUuid')->willReturn('user-uuid');\n\n return $user;\n }\n\n private function makeSavedSearch(array $filters): Search&MockObject\n {\n $savedSearch = $this->createMock(Search::class);\n $savedSearch->method('getId')->willReturn(42);\n $savedSearch->method('getFilters')->willReturn(new \\Illuminate\\Support\\LazyCollection($filters));\n\n return $savedSearch;\n }\n\n public function testGetActivityIdsForSavedSearchReturnsIds(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->expects($this->once())\n ->method('getArrayFilterKeys')\n ->with($user)\n ->willReturn([]);\n\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n\n $this->elasticRepository->expects($this->once())\n ->method('onDemandSearchIdsOnly')\n ->willReturn(['id-1', 'id-2', 'id-3']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with('[AskJiminnyReport] Fetched activity IDs for saved search');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1', 'id-2', 'id-3'], $result);\n }\n\n public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n\n $this->logger->expects($this->once())->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEmpty($result);\n }\n\n public function testGetActivityIdsFiltersOutDateFilters(): void\n {\n $user = $this->makeUser();\n\n $nonDateFilter = $this->makeFilter('owner_id', '123');\n $startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');\n $endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');\n $updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');\n $updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');\n\n $savedSearch = $this->makeSavedSearch([\n $nonDateFilter,\n $startDateFilter,\n $endDateFilter,\n $updatedFromFilter,\n $updatedToFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n }\n\n public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void\n {\n $user = $this->makeUser();\n\n $closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');\n $closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');\n $regularFilter = $this->makeFilter('rep_id', '99');\n\n $savedSearch = $this->makeSavedSearch([\n $closingStartFilter,\n $closingEndFilter,\n $regularFilter,\n ]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesArrayFilters(): void\n {\n $user = $this->makeUser();\n\n $filter1 = $this->makeFilter('outcome', 'positive');\n $filter2 = $this->makeFilter('outcome', 'negative');\n\n $savedSearch = $this->makeSavedSearch([$filter1, $filter2]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-1'], $result);\n }\n\n public function testGetActivityIdsHandlesScalarFilters(): void\n {\n $user = $this->makeUser();\n\n $filter = $this->makeFilter('direction', 'inbound');\n $savedSearch = $this->makeSavedSearch([$filter]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);\n $this->logger->method('info');\n\n $result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertEquals(['id-5'], $result);\n }\n\n public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n\n $capturedCriteria = null;\n $this->activitySearch->expects($this->once())\n ->method('getOnDemandPageFilterSet')\n ->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {\n $capturedCriteria = $criteria;\n\n return $filterSet;\n });\n\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);\n $this->logger->method('info');\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n\n $this->assertNotNull($capturedCriteria);\n $this->assertFalse($capturedCriteria->isFirstRequest());\n }\n\n public function testGetActivityIdsLogsWithCorrectContext(): void\n {\n $user = $this->makeUser();\n $savedSearch = $this->makeSavedSearch([]);\n\n $filterSet = $this->createMock(FilterDefinitionCollection::class);\n\n $this->activitySearch->method('getArrayFilterKeys')->willReturn([]);\n $this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);\n $this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);\n\n $this->logger->expects($this->once())\n ->method('info')\n ->with(\n '[AskJiminnyReport] Fetched activity IDs for saved search',\n $this->callback(fn ($context) => $context['saved_search_id'] === 42\n && $context['user_id'] === 1\n && $context['activity_count'] === 2)\n );\n\n $this->service->getActivityIdsForSavedSearch($savedSearch, $user);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports \n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). \n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports \n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). \n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5719932180597039997
|
2960206221971779892
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Kiosk\AutomatedReports;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityActualDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\ActivityUpdatedDate;
use Jiminny\Component\ActivitySearch\FilterDefinition\DealInsights\ClosingPeriodFilter;
use Jiminny\Component\ActivitySearch\FilterDefinitionCollection;
use Jiminny\Component\ActivitySearch\Service\ActivitySearch;
use Jiminny\Models\Activity\Search;
use Jiminny\Models\Activity\SearchFilter;
use Jiminny\Models\User;
use Jiminny\Repositories\ElasticActivityRepository;
use Jiminny\Services\Kiosk\AutomatedReports\AskJiminnyReportActivityService;
use Jiminny\VO\Repository\OnDemandActivitySearch\Criteria;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AskJiminnyReportActivityServiceTest extends TestCase
{
private ActivitySearch&MockObject $activitySearch;
private ElasticActivityRepository&MockObject $elasticRepository;
private LoggerInterface&MockObject $logger;
private AskJiminnyReportActivityService $service;
protected function setUp(): void
{
$this->activitySearch = $this->createMock(ActivitySearch::class);
$this->elasticRepository = $this->createMock(ElasticActivityRepository::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AskJiminnyReportActivityService(
$this->activitySearch,
$this->elasticRepository,
$this->logger,
);
}
private function makeFilter(string $key, ?string $value): SearchFilter&MockObject
{
$filter = $this->createMock(SearchFilter::class);
$filter->method('getFilterProperty')->willReturn($key);
$filter->method('getFilterValue')->willReturn($value);
return $filter;
}
private function makeUser(): User&MockObject
{
$tz = new \DateTimeZone('UTC');
$user = $this->createMock(User::class);
$user->method('getTimezone')->willReturn($tz);
$user->method('getId')->willReturn(1);
$user->method('getUuid')->willReturn('user-uuid');
return $user;
}
private function makeSavedSearch(array $filters): Search&MockObject
{
$savedSearch = $this->createMock(Search::class);
$savedSearch->method('getId')->willReturn(42);
$savedSearch->method('getFilters')->willReturn(new \Illuminate\Support\LazyCollection($filters));
return $savedSearch;
}
public function testGetActivityIdsForSavedSearchReturnsIds(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->expects($this->once())
->method('getArrayFilterKeys')
->with($user)
->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->expects($this->once())
->method('onDemandSearchIdsOnly')
->willReturn(['id-1', 'id-2', 'id-3']);
$this->logger->expects($this->once())
->method('info')
->with('[AskJiminnyReport] Fetched activity IDs for saved search');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1', 'id-2', 'id-3'], $result);
}
public function testGetActivityIdsForSavedSearchReturnsEmptyWhenNoResults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->expects($this->once())->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEmpty($result);
}
public function testGetActivityIdsFiltersOutDateFilters(): void
{
$user = $this->makeUser();
$nonDateFilter = $this->makeFilter('owner_id', '123');
$startDateFilter = $this->makeFilter(ActivityActualDate::PARAM_START_DATE, '2025-01-01 00:00:00');
$endDateFilter = $this->makeFilter(ActivityActualDate::PARAM_END_DATE, '2025-01-31 23:59:59');
$updatedFromFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_FROM, '2025-01-01 00:00:00');
$updatedToFilter = $this->makeFilter(ActivityUpdatedDate::PARAM_UPDATED_TO, '2025-01-31 23:59:59');
$savedSearch = $this->makeSavedSearch([
$nonDateFilter,
$startDateFilter,
$endDateFilter,
$updatedFromFilter,
$updatedToFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
}
public function testGetActivityIdsFiltersOutClosingPeriodDateFilters(): void
{
$user = $this->makeUser();
$closingStartFilter = $this->makeFilter(ClosingPeriodFilter::KEY_START_DATE, '2025-01-01');
$closingEndFilter = $this->makeFilter(ClosingPeriodFilter::KEY_END_DATE, '2025-03-31');
$regularFilter = $this->makeFilter('rep_id', '99');
$savedSearch = $this->makeSavedSearch([
$closingStartFilter,
$closingEndFilter,
$regularFilter,
]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesArrayFilters(): void
{
$user = $this->makeUser();
$filter1 = $this->makeFilter('outcome', 'positive');
$filter2 = $this->makeFilter('outcome', 'negative');
$savedSearch = $this->makeSavedSearch([$filter1, $filter2]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn(['outcome']);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-1']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-1'], $result);
}
public function testGetActivityIdsHandlesScalarFilters(): void
{
$user = $this->makeUser();
$filter = $this->makeFilter('direction', 'inbound');
$savedSearch = $this->makeSavedSearch([$filter]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['id-5']);
$this->logger->method('info');
$result = $this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertEquals(['id-5'], $result);
}
public function testGetActivityIdsPassesNonZeroSequenceNumberToDisableFirstRequestDefaults(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$capturedCriteria = null;
$this->activitySearch->expects($this->once())
->method('getOnDemandPageFilterSet')
->willReturnCallback(function (Criteria $criteria) use ($filterSet, &$capturedCriteria) {
$capturedCriteria = $criteria;
return $filterSet;
});
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn([]);
$this->logger->method('info');
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
$this->assertNotNull($capturedCriteria);
$this->assertFalse($capturedCriteria->isFirstRequest());
}
public function testGetActivityIdsLogsWithCorrectContext(): void
{
$user = $this->makeUser();
$savedSearch = $this->makeSavedSearch([]);
$filterSet = $this->createMock(FilterDefinitionCollection::class);
$this->activitySearch->method('getArrayFilterKeys')->willReturn([]);
$this->activitySearch->method('getOnDemandPageFilterSet')->willReturn($filterSet);
$this->elasticRepository->method('onDemandSearchIdsOnly')->willReturn(['a', 'b']);
$this->logger->expects($this->once())
->method('info')
->with(
'[AskJiminnyReport] Fetched activity IDs for saved search',
$this->callback(fn ($context) => $context['saved_search_id'] === 42
&& $context['user_id'] === 1
&& $context['activity_count'] === 2)
);
$this->service->getActivityIdsForSavedSearch($savedSearch, $user);
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
11207
|
|
11213
|
222
|
6
|
2026-04-14T09:19:59.366418+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158399366_m1.jpg...
|
iTerm2
|
ec2-user@ip-10-30-93-249:~
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys007
Poetry Last login: Sat Apr 11 12:38:35 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ vstg
Warning: Permanently added 'jiminny-stage-ecs1' (ED25519) to the list of known hosts.
A newer release of "Amazon Linux" is available.
Version 2023.10.20260105:
Version 2023.10.20260120:
Version 2023.10.20260202:
Version 2023.10.20260216:
Version 2023.10.20260302:
Version 2023.10.20260325:
Version 2023.10.20260330:
Version 2023.11.20260406:
Version 2023.11.20260413:
Version 2023.8.20250707:
Version 2023.8.20250715:
Version 2023.8.20250721:
Version 2023.8.20250808:
Version 2023.8.20250818:
Version 2023.8.20250908:
Version 2023.8.20250915:
Version 2023.9.20250929:
Version 2023.9.20251014:
Version 2023.9.20251020:
Version 2023.9.20251027:
Version 2023.9.20251105:
Version 2023.9.20251110:
Version 2023.9.20251117:
Version 2023.9.20251208:
Run "/usr/bin/dnf check-release-update" for full release and version update info
, #_
~\_ ####_
~~ \_#####\
~~ \###|
~~ \#/ ___ Amazon Linux 2023 (ECS Optimized)
~~ V~' '->
~~~ /
~~._. _/
_/ _/
_/m/'
For documentation, visit [URL_WITH_CREDENTIALS] php artisan automated-reports
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Started {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Checking conditions {"isMonday":false,"isFirstDayOfMonth":false,"currentMonth":4,"isQuarterlyMonth":true} {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Processing daily reports {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Found 2 daily reports to process {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"fa7417aa-538e-49ab-8827-77235637a6f9","teamId":1,"frequency":"daily","type":"ask_jiminny"} {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"63e6d70b-b7cb-4dfa-8443-53453e6c4054","teamId":1,"frequency":"daily","type":"ask_jiminny"} {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Completed {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
root@fee51d2e1f17:/home/jiminny# [ec2-user@ip-10-30-93-249 ~]$ docker exec -it $(docker ps --format "{{.ID}}" --filter "name=ecs-worker" | head -1) /bin/bash -c "cd /home/jiminny && bash"
root@73b64f5d54a3:/home/jiminny# php artisan automated-reports
[2026-04-14 08:41:03] staging.INFO: [automated-reports] Started {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:03] staging.INFO: [automated-reports] Checking conditions {"isMonday":false,"isFirstDayOfMonth":false,"currentMonth":4,"isQuarterlyMonth":true} {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:03] staging.INFO: [automated-reports] Processing daily reports {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:03] staging.INFO: [automated-reports] Found 3 daily reports to process {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:03] staging.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"fa7417aa-538e-49ab-8827-77235637a6f9","teamId":1,"frequency":"daily","type":"ask_jiminny"} {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:03] staging.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"63e6d70b-b7cb-4dfa-8443-53453e6c4054","teamId":1,"frequency":"daily","type":"ask_jiminny"} {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:04] staging.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"7e7846e2-c0ea-4040-88f4-0ae14b66ade8","teamId":1,"frequency":"daily","type":"ask_jiminny"} {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:04] staging.INFO: [automated-reports] Completed {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
root@73b64f5d54a3:/home/jiminny# [ec2-user@ip-10-30-93-249 ~]$
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
ec2-user@ip-10-30-93-249:~...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ vstg\nWarning: Permanently added 'jiminny-stage-ecs1' (ED25519) to the list of known hosts.\n\nA newer release of \"Amazon Linux\" is available.\n Version 2023.10.20260105:\n Version 2023.10.20260120:\n Version 2023.10.20260202:\n Version 2023.10.20260216:\n Version 2023.10.20260302:\n Version 2023.10.20260325:\n Version 2023.10.20260330:\n Version 2023.11.20260406:\n Version 2023.11.20260413:\n Version 2023.8.20250707:\n Version 2023.8.20250715:\n Version 2023.8.20250721:\n Version 2023.8.20250808:\n Version 2023.8.20250818:\n Version 2023.8.20250908:\n Version 2023.8.20250915:\n Version 2023.9.20250929:\n Version 2023.9.20251014:\n Version 2023.9.20251020:\n Version 2023.9.20251027:\n Version 2023.9.20251105:\n Version 2023.9.20251110:\n Version 2023.9.20251117:\n Version 2023.9.20251208:\nRun \"/usr/bin/dnf check-release-update\" for full release and version update info\n , #_\n ~\\_ ####_\n ~~ \\_#####\\\n ~~ \\###|\n ~~ \\#/ ___ Amazon Linux 2023 (ECS Optimized)\n ~~ V~' '->\n ~~~ /\n ~~._. _/\n _/ _/\n _/m/'\n\nFor documentation, visit http://aws.amazon.com/documentation/ecs\n[ec2-user@ip-10-30-93-249 ~]$ docker exec -it $(docker ps --format \"{{.ID}}\" --filter \"name=ecs-worker\" | head -1) /bin/bash -c \"cd /home/jiminny && bash\"\nroot@fee51d2e1f17:/home/jiminny# php artisan about\n\n Environment ...................................................................................................................................... \n Application Name ................................................................................................................. Jiminny Web App \n Laravel Version .......................................................................................................................... 12.54.1 \n PHP Version ............................................................................................................................... 8.3.30 \n Composer Version ............................................................................................................................... - \n Environment .............................................................................................................................. staging \n Debug Mode ................................................................................................................................... OFF \n URL ...................................................................................................................... app.staging.jiminny.com \n Maintenance Mode ............................................................................................................................. OFF \n Timezone ..................................................................................................................................... UTC \n Locale ..................................................................................................................................... en_US \n\n Cache ............................................................................................................................................ \n Config .................................................................................................................................... CACHED \n Events ................................................................................................................................ NOT CACHED \n Routes .................................................................................................................................... CACHED \n Views ..................................................................................................................................... CACHED \n\n Drivers .......................................................................................................................................... \n Broadcasting .............................................................................................................................. pusher \n Cache ...................................................................................................................................... redis \n Database ................................................................................................................................... mysql \n Logs .................................................................................................................................... errorlog \n Mail ......................................................................................................................................... ses \n Queue ........................................................................................................................................ sqs \n Session .................................................................................................................................... redis \n\n Storage .......................................................................................................................................... \n public/storage ........................................................................................................................ NOT LINKED \n\n Sentry ........................................................................................................................................... \n Enabled ...................................................................................................................................... YES \n Environment .............................................................................................................................. staging \n Laravel SDK Version ....................................................................................................................... 4.13.0 \n PHP SDK Version ........................................................................................................................... 4.13.0 \n Release ................................................................................................................................... 869720 \n Sample Rate Errors .......................................................................................................................... 100% \n Sample Rate Performance Monitoring ....................................................................................................... NOT SET \n Sample Rate Profiling .................................................................................................................... NOT SET \n Send Default PII ........................................................................................................................ DISABLED \n\nroot@fee51d2e1f17:/home/jiminny# php artisan automated-reports\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Started {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Checking conditions {\"isMonday\":false,\"isFirstDayOfMonth\":false,\"currentMonth\":4,\"isQuarterlyMonth\":true} {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Processing daily reports {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Found 2 daily reports to process {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"fa7417aa-538e-49ab-8827-77235637a6f9\",\"teamId\":1,\"frequency\":\"daily\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"63e6d70b-b7cb-4dfa-8443-53453e6c4054\",\"teamId\":1,\"frequency\":\"daily\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Completed {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\nroot@fee51d2e1f17:/home/jiminny# [ec2-user@ip-10-30-93-249 ~]$ docker exec -it $(docker ps --format \"{{.ID}}\" --filter \"name=ecs-worker\" | head -1) /bin/bash -c \"cd /home/jiminny && bash\"\nroot@73b64f5d54a3:/home/jiminny# php artisan automated-reports\n[2026-04-14 08:41:03] staging.INFO: [automated-reports] Started {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:03] staging.INFO: [automated-reports] Checking conditions {\"isMonday\":false,\"isFirstDayOfMonth\":false,\"currentMonth\":4,\"isQuarterlyMonth\":true} {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:03] staging.INFO: [automated-reports] Processing daily reports {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:03] staging.INFO: [automated-reports] Found 3 daily reports to process {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:03] staging.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"fa7417aa-538e-49ab-8827-77235637a6f9\",\"teamId\":1,\"frequency\":\"daily\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:03] staging.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"63e6d70b-b7cb-4dfa-8443-53453e6c4054\",\"teamId\":1,\"frequency\":\"daily\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:04] staging.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"7e7846e2-c0ea-4040-88f4-0ae14b66ade8\",\"teamId\":1,\"frequency\":\"daily\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:04] staging.INFO: [automated-reports] Completed {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\nroot@73b64f5d54a3:/home/jiminny# [ec2-user@ip-10-30-93-249 ~]$","depth":4,"value":"Last login: Sat Apr 11 12:38:35 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ vstg\nWarning: Permanently added 'jiminny-stage-ecs1' (ED25519) to the list of known hosts.\n\nA newer release of \"Amazon Linux\" is available.\n Version 2023.10.20260105:\n Version 2023.10.20260120:\n Version 2023.10.20260202:\n Version 2023.10.20260216:\n Version 2023.10.20260302:\n Version 2023.10.20260325:\n Version 2023.10.20260330:\n Version 2023.11.20260406:\n Version 2023.11.20260413:\n Version 2023.8.20250707:\n Version 2023.8.20250715:\n Version 2023.8.20250721:\n Version 2023.8.20250808:\n Version 2023.8.20250818:\n Version 2023.8.20250908:\n Version 2023.8.20250915:\n Version 2023.9.20250929:\n Version 2023.9.20251014:\n Version 2023.9.20251020:\n Version 2023.9.20251027:\n Version 2023.9.20251105:\n Version 2023.9.20251110:\n Version 2023.9.20251117:\n Version 2023.9.20251208:\nRun \"/usr/bin/dnf check-release-update\" for full release and version update info\n , #_\n ~\\_ ####_\n ~~ \\_#####\\\n ~~ \\###|\n ~~ \\#/ ___ Amazon Linux 2023 (ECS Optimized)\n ~~ V~' '->\n ~~~ /\n ~~._. _/\n _/ _/\n _/m/'\n\nFor documentation, visit http://aws.amazon.com/documentation/ecs\n[ec2-user@ip-10-30-93-249 ~]$ docker exec -it $(docker ps --format \"{{.ID}}\" --filter \"name=ecs-worker\" | head -1) /bin/bash -c \"cd /home/jiminny && bash\"\nroot@fee51d2e1f17:/home/jiminny# php artisan about\n\n Environment ...................................................................................................................................... \n Application Name ................................................................................................................. Jiminny Web App \n Laravel Version .......................................................................................................................... 12.54.1 \n PHP Version ............................................................................................................................... 8.3.30 \n Composer Version ............................................................................................................................... - \n Environment .............................................................................................................................. staging \n Debug Mode ................................................................................................................................... OFF \n URL ...................................................................................................................... app.staging.jiminny.com \n Maintenance Mode ............................................................................................................................. OFF \n Timezone ..................................................................................................................................... UTC \n Locale ..................................................................................................................................... en_US \n\n Cache ............................................................................................................................................ \n Config .................................................................................................................................... CACHED \n Events ................................................................................................................................ NOT CACHED \n Routes .................................................................................................................................... CACHED \n Views ..................................................................................................................................... CACHED \n\n Drivers .......................................................................................................................................... \n Broadcasting .............................................................................................................................. pusher \n Cache ...................................................................................................................................... redis \n Database ................................................................................................................................... mysql \n Logs .................................................................................................................................... errorlog \n Mail ......................................................................................................................................... ses \n Queue ........................................................................................................................................ sqs \n Session .................................................................................................................................... redis \n\n Storage .......................................................................................................................................... \n public/storage ........................................................................................................................ NOT LINKED \n\n Sentry ........................................................................................................................................... \n Enabled ...................................................................................................................................... YES \n Environment .............................................................................................................................. staging \n Laravel SDK Version ....................................................................................................................... 4.13.0 \n PHP SDK Version ........................................................................................................................... 4.13.0 \n Release ................................................................................................................................... 869720 \n Sample Rate Errors .......................................................................................................................... 100% \n Sample Rate Performance Monitoring ....................................................................................................... NOT SET \n Sample Rate Profiling .................................................................................................................... NOT SET \n Send Default PII ........................................................................................................................ DISABLED \n\nroot@fee51d2e1f17:/home/jiminny# php artisan automated-reports\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Started {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Checking conditions {\"isMonday\":false,\"isFirstDayOfMonth\":false,\"currentMonth\":4,\"isQuarterlyMonth\":true} {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Processing daily reports {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Found 2 daily reports to process {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"fa7417aa-538e-49ab-8827-77235637a6f9\",\"teamId\":1,\"frequency\":\"daily\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"63e6d70b-b7cb-4dfa-8443-53453e6c4054\",\"teamId\":1,\"frequency\":\"daily\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\n[2026-04-14 07:48:51] staging.INFO: [automated-reports] Completed {\"correlation_id\":\"4c37ea47-eebd-4122-8c35-9d6b9d707beb\",\"trace_id\":\"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81\"}\n\nroot@fee51d2e1f17:/home/jiminny# [ec2-user@ip-10-30-93-249 ~]$ docker exec -it $(docker ps --format \"{{.ID}}\" --filter \"name=ecs-worker\" | head -1) /bin/bash -c \"cd /home/jiminny && bash\"\nroot@73b64f5d54a3:/home/jiminny# php artisan automated-reports\n[2026-04-14 08:41:03] staging.INFO: [automated-reports] Started {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:03] staging.INFO: [automated-reports] Checking conditions {\"isMonday\":false,\"isFirstDayOfMonth\":false,\"currentMonth\":4,\"isQuarterlyMonth\":true} {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:03] staging.INFO: [automated-reports] Processing daily reports {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:03] staging.INFO: [automated-reports] Found 3 daily reports to process {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:03] staging.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"fa7417aa-538e-49ab-8827-77235637a6f9\",\"teamId\":1,\"frequency\":\"daily\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:03] staging.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"63e6d70b-b7cb-4dfa-8443-53453e6c4054\",\"teamId\":1,\"frequency\":\"daily\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:04] staging.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"7e7846e2-c0ea-4040-88f4-0ae14b66ade8\",\"teamId\":1,\"frequency\":\"daily\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\n[2026-04-14 08:41:04] staging.INFO: [automated-reports] Completed {\"correlation_id\":\"c858e03f-62bd-462d-add2-c1e12a4c4cf8\",\"trace_id\":\"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f\"}\n\nroot@73b64f5d54a3:/home/jiminny# [ec2-user@ip-10-30-93-249 ~]$","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"ec2-user@ip-10-30-93-249:~","depth":1,"bounds":{"left":0.4298611,"top":0.033333335,"width":0.13958333,"height":0.017777778},"role_description":"text"}]...
|
-4053156335861098651
|
-1172590465133121645
|
visual_change
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys007
Poetry Last login: Sat Apr 11 12:38:35 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ vstg
Warning: Permanently added 'jiminny-stage-ecs1' (ED25519) to the list of known hosts.
A newer release of "Amazon Linux" is available.
Version 2023.10.20260105:
Version 2023.10.20260120:
Version 2023.10.20260202:
Version 2023.10.20260216:
Version 2023.10.20260302:
Version 2023.10.20260325:
Version 2023.10.20260330:
Version 2023.11.20260406:
Version 2023.11.20260413:
Version 2023.8.20250707:
Version 2023.8.20250715:
Version 2023.8.20250721:
Version 2023.8.20250808:
Version 2023.8.20250818:
Version 2023.8.20250908:
Version 2023.8.20250915:
Version 2023.9.20250929:
Version 2023.9.20251014:
Version 2023.9.20251020:
Version 2023.9.20251027:
Version 2023.9.20251105:
Version 2023.9.20251110:
Version 2023.9.20251117:
Version 2023.9.20251208:
Run "/usr/bin/dnf check-release-update" for full release and version update info
, #_
~\_ ####_
~~ \_#####\
~~ \###|
~~ \#/ ___ Amazon Linux 2023 (ECS Optimized)
~~ V~' '->
~~~ /
~~._. _/
_/ _/
_/m/'
For documentation, visit [URL_WITH_CREDENTIALS] php artisan automated-reports
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Started {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Checking conditions {"isMonday":false,"isFirstDayOfMonth":false,"currentMonth":4,"isQuarterlyMonth":true} {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Processing daily reports {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Found 2 daily reports to process {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"fa7417aa-538e-49ab-8827-77235637a6f9","teamId":1,"frequency":"daily","type":"ask_jiminny"} {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"63e6d70b-b7cb-4dfa-8443-53453e6c4054","teamId":1,"frequency":"daily","type":"ask_jiminny"} {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
[2026-04-14 07:48:51] staging.INFO: [automated-reports] Completed {"correlation_id":"4c37ea47-eebd-4122-8c35-9d6b9d707beb","trace_id":"bfe6b131-e3ad-4cfc-8954-5fb1ecfded81"}
root@fee51d2e1f17:/home/jiminny# [ec2-user@ip-10-30-93-249 ~]$ docker exec -it $(docker ps --format "{{.ID}}" --filter "name=ecs-worker" | head -1) /bin/bash -c "cd /home/jiminny && bash"
root@73b64f5d54a3:/home/jiminny# php artisan automated-reports
[2026-04-14 08:41:03] staging.INFO: [automated-reports] Started {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:03] staging.INFO: [automated-reports] Checking conditions {"isMonday":false,"isFirstDayOfMonth":false,"currentMonth":4,"isQuarterlyMonth":true} {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:03] staging.INFO: [automated-reports] Processing daily reports {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:03] staging.INFO: [automated-reports] Found 3 daily reports to process {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:03] staging.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"fa7417aa-538e-49ab-8827-77235637a6f9","teamId":1,"frequency":"daily","type":"ask_jiminny"} {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:03] staging.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"63e6d70b-b7cb-4dfa-8443-53453e6c4054","teamId":1,"frequency":"daily","type":"ask_jiminny"} {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:04] staging.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"7e7846e2-c0ea-4040-88f4-0ae14b66ade8","teamId":1,"frequency":"daily","type":"ask_jiminny"} {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
[2026-04-14 08:41:04] staging.INFO: [automated-reports] Completed {"correlation_id":"c858e03f-62bd-462d-add2-c1e12a4c4cf8","trace_id":"94b4fdcc-f609-42e7-b5b7-b6abfc67e02f"}
root@73b64f5d54a3:/home/jiminny# [ec2-user@ip-10-30-93-249 ~]$
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
ec2-user@ip-10-30-93-249:~...
|
11211
|
|
11214
|
222
|
7
|
2026-04-14T09:20:00.953718+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158400953_m1.jpg...
|
iTerm2
|
DEV (-zsh)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (-zsh)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $","depth":4,"bounds":{"left":0.0,"top":0.08777778,"width":0.9895833,"height":0.9122222},"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (-zsh)","depth":1,"bounds":{"left":0.47430557,"top":0.033333335,"width":0.052083332,"height":0.017777778},"role_description":"text"}]...
|
1132831535450885273
|
5729594858220514453
|
click
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (-zsh)...
|
NULL
|
|
11215
|
223
|
6
|
2026-04-14T09:20:00.967895+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158400967_m2.jpg...
|
iTerm2
|
DEV (-zsh)
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (-zsh)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.5566406,"height":-0.05486107},"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.23320313,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.23554687,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.30234376,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3046875,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.37148437,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3738281,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.440625,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.44296876,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5097656,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5121094,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.57890624,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.58125,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.64804685,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6503906,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.7171875,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.71953124,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7703125,"top":1.0,"width":0.021875,"height":-0.02013886},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (-zsh)","depth":1,"bounds":{"left":0.5,"top":1.0,"width":0.029296875,"height":-0.020833373},"role_description":"text"}]...
|
1132831535450885273
|
5729594858220514453
|
click
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (-zsh)...
|
NULL
|
|
11218
|
222
|
9
|
2026-04-14T09:20:14.633578+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158414633_m1.jpg...
|
Raycast
|
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Today
automated-reports
private function processRe Today
automated-reports
private function processReports(string $frequency): void…
[URL_WITH_CREDENTIALS] ~ $ curl -s -X POST [URL_WITH_CREDENTIALS] ~ $ curl -s -X POST http://localhost:3030/raw_sql \…...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Today","depth":5,"bounds":{"left":0.48055556,"top":0.38555557,"width":0.027777778,"height":0.016666668},"role_description":"text"},{"role":"AXStaticText","text":"automated-reports","depth":5,"bounds":{"left":0.50277776,"top":0.42333335,"width":0.084375,"height":0.02},"role_description":"text"},{"role":"AXStaticText","text":"private function processReports(string $frequency): void…","depth":5,"bounds":{"left":0.50277776,"top":0.4677778,"width":0.15625,"height":0.02},"role_description":"text"},{"role":"AXStaticText","text":"https://us-east-2.signin.aws.amazon.com/oauth?client_id=arn%3Aaws%3Asignin%3A%3A%3Aconsole%2Fcloudwatch&code_challenge=Fdax6Rv9vU0rCRlxoWmmhXnxKIKJ3gNUH98LMj4v_jw&code_challenge_method=SHA-256&response_type=code&redirect_uri=https%3A%2F%2Fus-east-2.console.aws.amazon.com%2Fcloudwatch%2Fhome%3Fca-oauth-flow-id%3D847b%26hashArgs%3D%2523logsV2%253Alogs-insights%25243FqueryDetail%25243D%257E%2528end%257E0%257Estart%257E-3600%257EtimeType%257E%2527RELATIVE%257Etz%257E%2527UTC%257Eunit%257E%2527seconds%257EeditorString%257E%2527fields*20*40timestamp*2c*20*40message*2c*20*40logStream*2c*20*40log*0a*7c*20filter*20*40message*20like*20*2fXXXXX*2f*20*0a*7c*20filter*20*40message*20not*20like*20*2fAnalytic*2f*20*7c*20filter*20*40message*20not*20like*20*2fTranscript*2f*0a*7c*20filter*20*40message*20not*20like*20*2fWebhook*2f*20*7c*20filter*20*40message*20not*20like*20*2fMeetingBot*2f*20*0a*7c*20limit*2010000%257EqueryId%257E%25270551e814-f51a-4339-8372-80d7ba4cef27%257Esource%257E%2528%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-analytics%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-app%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-audio%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-calendar%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-conferences%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-crm-sync%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-default%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-delayed%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-dialers%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-dialers-fifo%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-download%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-emails%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-meeting-bot%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-nudges%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-processing-1%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-processing-2%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-processing-3%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-processing-4%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-processing-5%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-processing-delayed%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-softphone%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-video%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aworker-video-app%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aphp%257E%2527arn*3aaws*3alogs*3aus-east-2*3a410346195943*3alog-group*3aphp-app%2529%257Elang%257E%2527CWLI%257ElogClass%257E%2527STANDARD%257EqueryBy%257E%2527logGroupName%2529%26isauthcode%3Dtrue%26oauthStart%3D1776153363251%26region%3Dus-east-2%26state%3DhashArgsFromTB_us-east-2_bf524caeb24e5caf","depth":5,"bounds":{"left":0.50277776,"top":0.51222223,"width":0.15625,"height":0.02},"role_description":"text"},{"role":"AXStaticText","text":"Here is an analysis of potential issues, bugs, and design flaws present in the provided code diff.…","depth":5,"bounds":{"left":0.50277776,"top":0.5566667,"width":0.15625,"height":0.02},"role_description":"text"},{"role":"AXStaticText","text":"LOG_LEVEL=DEBUG…","depth":5,"bounds":{"left":0.50277776,"top":0.6011111,"width":0.099305555,"height":0.02},"role_description":"text"},{"role":"AXStaticText","text":"https://meet.google.com/mie-gawc-dsi?authuser=lukas.kovalik%40jiminny.com","depth":5,"bounds":{"left":0.50277776,"top":0.64555556,"width":0.15625,"height":0.02},"role_description":"text"},{"role":"AXStaticText","text":"Yesterday","depth":5,"bounds":{"left":0.48055556,"top":0.69222224,"width":0.043402776,"height":0.016666668},"role_description":"text"},{"role":"AXStaticText","text":"Image (638x333)","depth":5,"bounds":{"left":0.50277776,"top":0.73,"width":0.078819446,"height":0.02},"role_description":"text"},{"role":"AXStaticText","text":"Image (1279x1234)","depth":5,"bounds":{"left":0.50277776,"top":0.77444446,"width":0.08576389,"height":0.02},"role_description":"text"},{"role":"AXStaticText","text":"Image (1324x1319)","depth":5,"bounds":{"left":0.50277776,"top":0.8188889,"width":0.084375,"height":0.02},"role_description":"text"},{"role":"AXStaticText","text":"Image (1318x320)","depth":5,"bounds":{"left":0.50277776,"top":0.86333334,"width":0.08055556,"height":0.02},"role_description":"text"},{"role":"AXStaticText","text":"Image (1875x1049)","depth":5,"bounds":{"left":0.50277776,"top":0.9077778,"width":0.08611111,"height":0.02},"role_description":"text"},{"role":"AXStaticText","text":"Image (1875x1049)","depth":5,"bounds":{"left":0.50277776,"top":0.9522222,"width":0.08611111,"height":0.02},"role_description":"text"},{"role":"AXStaticText","text":"This Month","depth":5,"bounds":{"left":0.48055556,"top":0.9988889,"width":0.04826389,"height":0.0011110902},"role_description":"text"},{"role":"AXStaticText","text":"lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ curl -s -X POST http://localhost:3030/raw_sql \\…","depth":5,"bounds":{"left":0.50277776,"top":1.0,"width":0.15625,"height":-0.03666663},"role_description":"text"},{"role":"AXStaticText","text":"lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ curl -s -X POST http://localhost:3030/raw_sql \\…","depth":5,"bounds":{"left":0.50277776,"top":1.0,"width":0.15625,"height":-0.08111107},"role_description":"text"}]...
|
-5985680639788096801
|
1154640625433064395
|
visual_change
|
accessibility
|
NULL
|
Today
automated-reports
private function processRe Today
automated-reports
private function processReports(string $frequency): void…
[URL_WITH_CREDENTIALS] ~ $ curl -s -X POST [URL_WITH_CREDENTIALS] ~ $ curl -s -X POST http://localhost:3030/raw_sql \…...
|
NULL
|
|
11219
|
222
|
10
|
2026-04-14T09:20:17.649190+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158417649_m1.jpg...
|
iTerm2
|
DEV (docker)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports","depth":4,"bounds":{"left":0.0,"top":0.08777778,"width":0.9895833,"height":0.9122222},"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.46875,"top":0.033333335,"width":0.0625,"height":0.017777778},"role_description":"text"}]...
|
149369918600881871
|
5009021114700230289
|
visual_change
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
11218
|
|
11225
|
223
|
11
|
2026-04-14T09:20:31.646332+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158431646_m2.jpg...
|
PhpStorm
|
faVsco.js – SF [jiminny@localhost]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Code changed:
Hide
Sync Changes
Hide This Notification
10
12
2
4
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select * from teams where id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM automated_reports where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
{"request_id":"edc47759-160a-4f61-82fc-c8cdf5e55464","status":"completed","timestamp":"2026-03-31T15:52:05.828229+00:00","s3_url":"s3:\/\/dev.jiminny.client-data\/5f0f4810-7e77-4086-8f69-93429ae4d70b\/panorama-reports\/test_req_q_1.MD","report_type":"exec_summary","pdf_url":"s3:\/\/dev.jiminny.client-data\/5f0f4810-7e77-4086-8f69-93429ae4d70b\/panorama-reports\/test_req_q_1.pdf"}
[URL_WITH_CREDENTIALS] 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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app
.circleci
.cursor
.github
.sonarlint
.vscode
.windsurf
app, sources root
Actions
Component
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.03046875,"top":0.017361112,"width":0.0453125,"height":0.022222223},"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#11894 on JY-18909-automated-reports-ask-jiminny, menu","depth":5,"bounds":{"left":0.07578125,"top":0.017361112,"width":0.14960937,"height":0.022222223},"help_text":"Pull request #11894 exists for current branch JY-18909-automated-reports-ask-jiminny, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.7589844,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceT…Defaults","depth":6,"bounds":{"left":0.7769531,"top":0.017361112,"width":0.12382813,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9007813,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'","depth":6,"bounds":{"left":0.9140625,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9273437,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96015626,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9734375,"top":0.017361112,"width":0.01328125,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9867188,"top":0.017361112,"width":0.013281226,"height":0.022222223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.6171875,"top":0.10763889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.6273438,"top":0.10763889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.64023435,"top":0.10763889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.6503906,"top":0.10763889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.6605469,"top":0.10763889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.6734375,"top":0.10763889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.6863281,"top":0.10763889,"width":0.028515626,"height":0.016666668},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.7175781,"top":0.10763889,"width":0.01015625,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.73046875,"top":0.10763889,"width":0.034765624,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9515625,"top":0.10763889,"width":0.033203125,"height":0.016666668},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10","depth":4,"bounds":{"left":0.91914064,"top":0.12916666,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.9328125,"top":0.12916666,"width":0.011328125,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.9464844,"top":0.12916666,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.95820314,"top":0.12916666,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.96953124,"top":0.12777779,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.978125,"top":0.12777779,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\nselect * from teams where id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM automated_reports where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\n{\"request_id\":\"edc47759-160a-4f61-82fc-c8cdf5e55464\",\"status\":\"completed\",\"timestamp\":\"2026-03-31T15:52:05.828229+00:00\",\"s3_url\":\"s3:\\/\\/dev.jiminny.client-data\\/5f0f4810-7e77-4086-8f69-93429ae4d70b\\/panorama-reports\\/test_req_q_1.MD\",\"report_type\":\"exec_summary\",\"pdf_url\":\"s3:\\/\\/dev.jiminny.client-data\\/5f0f4810-7e77-4086-8f69-93429ae4d70b\\/panorama-reports\\/test_req_q_1.pdf\"}\n\ns3://dev.jiminny.client-data/5f0f4810-7e77-4086-8f69-93429ae4d70b/panorama-reports/test_req_q_1.MD\ns3://dev.jiminny.client-data/5f0f4810-7e77-4086-8f69-93429ae4d70b/panorama-reports/test_req_q_1.pdf","depth":4,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\nselect * from teams where id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM automated_reports where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\n{\"request_id\":\"edc47759-160a-4f61-82fc-c8cdf5e55464\",\"status\":\"completed\",\"timestamp\":\"2026-03-31T15:52:05.828229+00:00\",\"s3_url\":\"s3:\\/\\/dev.jiminny.client-data\\/5f0f4810-7e77-4086-8f69-93429ae4d70b\\/panorama-reports\\/test_req_q_1.MD\",\"report_type\":\"exec_summary\",\"pdf_url\":\"s3:\\/\\/dev.jiminny.client-data\\/5f0f4810-7e77-4086-8f69-93429ae4d70b\\/panorama-reports\\/test_req_q_1.pdf\"}\n\ns3://dev.jiminny.client-data/5f0f4810-7e77-4086-8f69-93429ae4d70b/panorama-reports/test_req_q_1.MD\ns3://dev.jiminny.client-data/5f0f4810-7e77-4086-8f69-93429ae4d70b/panorama-reports/test_req_q_1.pdf","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.049609374,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.5875,"top":0.13055556,"width":0.009375,"height":0.013194445},"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.59882814,"top":0.12916666,"width":0.00859375,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.6074219,"top":0.12916666,"width":0.008203125,"height":0.015972223},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports \n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). \n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","depth":4,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Reports;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Contracts\\Bus\\Dispatcher as BusDispatcher;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateAskJiminnyReportJob;\nuse Jiminny\\Jobs\\AutomatedReports\\RequestGenerateReportJob;\nuse Jiminny\\Models\\AutomatedReport;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Repositories\\AutomatedReportsRepository;\nuse Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService;\nuse Psr\\Log\\LoggerInterface;\n\nclass AutomatedReportsCommand extends Command\n{\n /**\n * Log prefix for all log messages\n */\n private const string LOG_PREFIX = '[automated-reports]';\n\n /**\n * The name and signature of the console command.\n *\n * @var string\n */\n protected $signature = 'automated-reports \n {--report-id= : Process a specific report by ID or UUID (bypasses frequency scheduling)}';\n\n /**\n * The console command description.\n *\n * @var string\n */\n protected $description = 'Process automated reports based on their frequency (weekly, monthly, quarterly). \n Use --report-id to manually trigger a specific report by ID or UUID.';\n\n\n public function __construct(\n private readonly LoggerInterface $logger,\n private readonly BusDispatcher $dispatcher,\n private readonly AutomatedReportsRepository $reportRepository\n ) {\n parent::__construct();\n }\n\n /**\n * Execute the console command.\n *\n * @return int\n */\n public function handle(): int\n {\n $this->logger->info(self::LOG_PREFIX . ' Started');\n\n $now = Carbon::now();\n $isMonday = $now->isMonday();\n $isFirstDayOfMonth = $now->day === 1;\n $currentMonth = $now->month;\n\n // Check if the current month is a quarterly month (January, April, July, October)\n $isQuarterlyMonth = in_array($currentMonth, [1, 4, 7, 10], true);\n\n $this->logger->info(self::LOG_PREFIX . ' Checking conditions', [\n 'isMonday' => $isMonday,\n 'isFirstDayOfMonth' => $isFirstDayOfMonth,\n 'currentMonth' => $currentMonth,\n 'isQuarterlyMonth' => $isQuarterlyMonth,\n ]);\n\n // Process daily reports\n $this->processReports(AutomatedReportsService::FREQUENCY_DAILY);\n\n // Process weekly reports on Mondays\n if ($isMonday) {\n $this->processReports(AutomatedReportsService::FREQUENCY_WEEKLY);\n }\n\n // Process monthly reports on the first day of the month\n if ($isFirstDayOfMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_MONTHLY);\n }\n\n // Process quarterly reports on the first day of January, April, July, and October\n if ($isFirstDayOfMonth && $isQuarterlyMonth) {\n $this->processReports(AutomatedReportsService::FREQUENCY_QUARTERLY);\n }\n\n $this->logger->info(self::LOG_PREFIX . ' Completed');\n\n return 0;\n }\n\n /**\n * Process reports for a specific frequency.\n *\n * @param string $frequency\n *\n * @return void\n */\n private function processReports(string $frequency): void\n {\n $this->logger->info(self::LOG_PREFIX . \" Processing $frequency reports\");\n\n $reportId = $this->option('report-id');\n if ($reportId !== null) {\n $reports = $this->getReportById($reportId);\n } else {\n // Get all enabled, not deleted reports with active teams for the specified frequency\n $reports = $this->reportRepository->getActiveReportsByFrequency($frequency);\n }\n\n $this->logger->info(self::LOG_PREFIX . \" Found {$reports->count()} $frequency reports to process\");\n\n /** @var AutomatedReport $report */\n foreach ($reports as $report) {\n $this->logger->info(self::LOG_PREFIX . ' Dispatching Generate Report job for report', [\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'frequency' => $report->getFrequency(),\n 'type' => $report->getType(),\n ]);\n\n $job = $report->isAskJiminnyReport()\n ? new RequestGenerateAskJiminnyReportJob($report->getUuid())\n : new RequestGenerateReportJob($report->getUuid());\n\n $this->dispatcher->dispatch($job);\n }\n }\n\n private function getReportById(string $reportId): Collection\n {\n $report = $this->reportRepository->findByIdOrUuid($reportId);\n\n if ($report === null) {\n $this->logger->warning(self::LOG_PREFIX . ' Report not found for --report-id', ['reportId' => $reportId]);\n $this->warn(\"Report not found: {$reportId}\");\n\n return collect();\n }\n\n if (! $report->getStatus()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is inactive, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n ]);\n $this->warn('Report is inactive — processing anyway (manual override).');\n }\n\n $team = $report->getTeam();\n if ($team->getStatus() !== Team::STATUS_ACTIVE) {\n $this->logger->warning(self::LOG_PREFIX . ' Team is not active, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'teamId' => $report->getTeamId(),\n 'teamStatus' => $team->getStatus(),\n ]);\n $this->warn(\"Team #{$report->getTeamId()} is not active — processing anyway (manual override).\");\n }\n\n if ($report->isExpired()) {\n $this->logger->warning(self::LOG_PREFIX . ' Report is expired, processing anyway (manual override)', [\n 'reportId' => $reportId,\n 'reportUuid' => $report->getUuid(),\n 'expiresAt' => $report->getExpiresAt()?->toDateString(),\n ]);\n $this->warn('Report is expired (expires_at: ' . $report->getExpiresAt()?->toDateString() . ') — processing anyway (manual override).');\n }\n\n return collect([$report]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.0140625,"top":0.041666668,"width":0.028515626,"height":0.021527778},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.01015625,"height":0.0},"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app","depth":6,"role_description":"text"},{"role":"AXStaticText","text":".circleci","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".cursor","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".vscode","depth":7,"role_description":"text"},{"role":"AXStaticText","text":".windsurf","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"role_description":"text"},{"role":"AXStaticText","text":"Actions","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Component","depth":8,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"role_description":"text"}]...
|
-5927610008380917177
|
6470157055874699845
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#11894 on JY-18909-automa Project: faVsco.js, menu
#11894 on JY-18909-automated-reports-ask-jiminny, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceT…Defaults
Run 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
Debug 'AskJiminnyReportActivityServiceTest.tes…uenceNumberToDisableFirstRequestDefaults'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Code changed:
Hide
Sync Changes
Hide This Notification
10
12
2
4
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select * from teams where id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM automated_reports where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
{"request_id":"edc47759-160a-4f61-82fc-c8cdf5e55464","status":"completed","timestamp":"2026-03-31T15:52:05.828229+00:00","s3_url":"s3:\/\/dev.jiminny.client-data\/5f0f4810-7e77-4086-8f69-93429ae4d70b\/panorama-reports\/test_req_q_1.MD","report_type":"exec_summary","pdf_url":"s3:\/\/dev.jiminny.client-data\/5f0f4810-7e77-4086-8f69-93429ae4d70b\/panorama-reports\/test_req_q_1.pdf"}
[URL_WITH_CREDENTIALS] 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');
$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;
}
/**
* 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).');
}
return collect([$report]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app
.circleci
.cursor
.github
.sonarlint
.vscode
.windsurf
app, sources root
Actions
Component
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder...
|
11223
|
|
11230
|
222
|
15
|
2026-04-14T09:20:47.472499+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158447472_m1.jpg...
|
iTerm2
|
DEV (docker)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports","depth":4,"bounds":{"left":0.0,"top":0.08777778,"width":0.9895833,"height":0.9122222},"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.46875,"top":0.033333335,"width":0.0625,"height":0.017777778},"role_description":"text"}]...
|
149369918600881871
|
5009021114700230289
|
click
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
NULL
|
|
11231
|
223
|
14
|
2026-04-14T09:20:47.471864+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158447471_m2.jpg...
|
iTerm2
|
DEV (docker)
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.5566406,"height":-0.05486107},"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.23320313,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.23554687,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.30234376,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3046875,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.37148437,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3738281,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.440625,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.44296876,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5097656,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5121094,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.57890624,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.58125,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.64804685,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6503906,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.7171875,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.71953124,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7703125,"top":1.0,"width":0.021875,"height":-0.02013886},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.496875,"top":1.0,"width":0.03515625,"height":-0.020833373},"role_description":"text"}]...
|
149369918600881871
|
5009021114700230289
|
click
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
NULL
|
|
11232
|
222
|
16
|
2026-04-14T09:20:50.972205+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158450972_m1.jpg...
|
iTerm2
|
DEV (docker)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265","depth":4,"bounds":{"left":0.0,"top":0.08777778,"width":0.9895833,"height":0.9122222},"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.46875,"top":0.033333335,"width":0.0625,"height":0.017777778},"role_description":"text"}]...
|
-6585604228898112628
|
5738604256502705809
|
visual_change
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
11230
|
|
11233
|
222
|
17
|
2026-04-14T09:21:00.034813+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158460034_m1.jpg...
|
iTerm2
|
DEV (docker)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny#","depth":4,"bounds":{"left":0.0,"top":0.08777778,"width":0.9895833,"height":0.9122222},"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.46875,"top":0.033333335,"width":0.0625,"height":0.017777778},"role_description":"text"}]...
|
1019121305684409762
|
550527820152130705
|
visual_change
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
NULL
|
|
11234
|
223
|
15
|
2026-04-14T09:21:01.927486+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158461927_m2.jpg...
|
iTerm2
|
DEV (docker)
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny#","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.5566406,"height":-0.05486107},"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.23320313,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.23554687,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.30234376,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3046875,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.37148437,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3738281,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.440625,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.44296876,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5097656,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5121094,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.57890624,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.58125,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.64804685,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6503906,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.7171875,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.71953124,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7703125,"top":1.0,"width":0.021875,"height":-0.02013886},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.496875,"top":1.0,"width":0.03515625,"height":-0.020833373},"role_description":"text"}]...
|
1019121305684409762
|
550527820152130705
|
click
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
11231
|
|
11236
|
222
|
19
|
2026-04-14T09:21:03.032932+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158463032_m1.jpg...
|
iTerm2
|
DEV (docker)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny#","depth":4,"bounds":{"left":0.0,"top":0.08777778,"width":0.9895833,"height":0.9122222},"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.46875,"top":0.033333335,"width":0.0625,"height":0.017777778},"role_description":"text"}]...
|
1019121305684409762
|
550527820152130705
|
visual_change
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
NULL
|
|
11238
|
222
|
21
|
2026-04-14T09:21:06.067341+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158466067_m1.jpg...
|
iTerm2
|
DEV (docker)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all","depth":4,"bounds":{"left":0.0,"top":0.08777778,"width":0.9895833,"height":0.9122222},"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.46875,"top":0.033333335,"width":0.0625,"height":0.017777778},"role_description":"text"}]...
|
-1665124540628748504
|
550527820156325781
|
visual_change
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
NULL
|
|
11239
|
223
|
16
|
2026-04-14T09:21:33.891329+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158493891_m2.jpg...
|
iTerm2
|
DEV (docker)
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 41.20ms DONE
cache [PASSWORD_DOTS] 61.54ms DONE
compiled [PASSWORD_DOTS] 3.39ms DONE
events [PASSWORD_DOTS] 5.01ms DONE
routes [PASSWORD_DOTS] 3.37ms DONE
views [PASSWORD_DOTS] 43.92ms DONE
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 41.20ms DONE\n cache ............................................................................................................................... 61.54ms DONE\n compiled ............................................................................................................................. 3.39ms DONE\n events ............................................................................................................................... 5.01ms DONE\n routes ............................................................................................................................... 3.37ms DONE\n views ............................................................................................................................... 43.92ms DONE","depth":4,"bounds":{"left":0.23320313,"top":1.0,"width":0.5566406,"height":-0.05486107},"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 41.20ms DONE\n cache ............................................................................................................................... 61.54ms DONE\n compiled ............................................................................................................................. 3.39ms DONE\n events ............................................................................................................................... 5.01ms DONE\n routes ............................................................................................................................... 3.37ms DONE\n views ............................................................................................................................... 43.92ms DONE","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.23320313,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.23554687,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.30234376,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3046875,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.37148437,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.3738281,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.440625,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.44296876,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5097656,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5121094,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.57890624,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.58125,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.64804685,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6503906,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.7171875,"top":1.0,"width":0.06914063,"height":-0.03680551},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.71953124,"top":1.0,"width":0.00625,"height":-0.039583325},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7703125,"top":1.0,"width":0.021875,"height":-0.02013886},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.496875,"top":1.0,"width":0.03515625,"height":-0.020833373},"role_description":"text"}]...
|
2997950504001719161
|
5017957912142899863
|
idle
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 41.20ms DONE
cache [PASSWORD_DOTS] 61.54ms DONE
compiled [PASSWORD_DOTS] 3.39ms DONE
events [PASSWORD_DOTS] 5.01ms DONE
routes [PASSWORD_DOTS] 3.37ms DONE
views [PASSWORD_DOTS] 43.92ms DONE
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
NULL
|
|
11240
|
222
|
22
|
2026-04-14T09:21:36.176325+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158496176_m1.jpg...
|
iTerm2
|
DEV (docker)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 41.20ms DONE
cache [PASSWORD_DOTS] 61.54ms DONE
compiled [PASSWORD_DOTS] 3.39ms DONE
events [PASSWORD_DOTS] 5.01ms DONE
routes [PASSWORD_DOTS] 3.37ms DONE
views [PASSWORD_DOTS] 43.92ms DONE
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 41.20ms DONE\n cache ............................................................................................................................... 61.54ms DONE\n compiled ............................................................................................................................. 3.39ms DONE\n events ............................................................................................................................... 5.01ms DONE\n routes ............................................................................................................................... 3.37ms DONE\n views ............................................................................................................................... 43.92ms DONE","depth":4,"bounds":{"left":0.0,"top":0.08777778,"width":0.9895833,"height":0.9122222},"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 41.20ms DONE\n cache ............................................................................................................................... 61.54ms DONE\n compiled ............................................................................................................................. 3.39ms DONE\n events ............................................................................................................................... 5.01ms DONE\n routes ............................................................................................................................... 3.37ms DONE\n views ............................................................................................................................... 43.92ms DONE","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.46875,"top":0.033333335,"width":0.0625,"height":0.017777778},"role_description":"text"}]...
|
2997950504001719161
|
5017957912142899863
|
idle
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 41.20ms DONE
cache [PASSWORD_DOTS] 61.54ms DONE
compiled [PASSWORD_DOTS] 3.39ms DONE
events [PASSWORD_DOTS] 5.01ms DONE
routes [PASSWORD_DOTS] 3.37ms DONE
views [PASSWORD_DOTS] 43.92ms DONE
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
11238
|
|
11241
|
222
|
23
|
2026-04-14T09:21:54.606354+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158514606_m1.jpg...
|
iTerm2
|
DEV (docker)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 41.20ms DONE
cache [PASSWORD_DOTS] 61.54ms DONE
compiled [PASSWORD_DOTS] 3.39ms DONE
events [PASSWORD_DOTS] 5.01ms DONE
routes [PASSWORD_DOTS] 3.37ms DONE
views [PASSWORD_DOTS] 43.92ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-emails:worker-emails_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker:worker_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 41.20ms DONE\n cache ............................................................................................................................... 61.54ms DONE\n compiled ............................................................................................................................. 3.39ms DONE\n events ............................................................................................................................... 5.01ms DONE\n routes ............................................................................................................................... 3.37ms DONE\n views ............................................................................................................................... 43.92ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker:worker_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped","depth":4,"bounds":{"left":0.0,"top":0.08777778,"width":0.9895833,"height":0.9122222},"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 41.20ms DONE\n cache ............................................................................................................................... 61.54ms DONE\n compiled ............................................................................................................................. 3.39ms DONE\n events ............................................................................................................................... 5.01ms DONE\n routes ............................................................................................................................... 3.37ms DONE\n views ............................................................................................................................... 43.92ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker:worker_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.46875,"top":0.033333335,"width":0.0625,"height":0.017777778},"role_description":"text"}]...
|
-6268490107721211767
|
405988254343752469
|
visual_change
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 41.20ms DONE
cache [PASSWORD_DOTS] 61.54ms DONE
compiled [PASSWORD_DOTS] 3.39ms DONE
events [PASSWORD_DOTS] 5.01ms DONE
routes [PASSWORD_DOTS] 3.37ms DONE
views [PASSWORD_DOTS] 43.92ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-emails:worker-emails_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker:worker_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
NULL
|
|
11242
|
222
|
24
|
2026-04-14T09:21:57.611139+00:00
|
/Users/lukas/.screenpipe/data/data/2026-04-14/1776 /Users/lukas/.screenpipe/data/data/2026-04-14/1776158517611_m1.jpg...
|
iTerm2
|
DEV (docker)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 41.20ms DONE
cache [PASSWORD_DOTS] 61.54ms DONE
compiled [PASSWORD_DOTS] 3.39ms DONE
events [PASSWORD_DOTS] 5.01ms DONE
routes [PASSWORD_DOTS] 3.37ms DONE
views [PASSWORD_DOTS] 43.92ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-emails:worker-emails_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker:worker_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 41.20ms DONE\n cache ............................................................................................................................... 61.54ms DONE\n compiled ............................................................................................................................. 3.39ms DONE\n events ............................................................................................................................... 5.01ms DONE\n routes ............................................................................................................................... 3.37ms DONE\n views ............................................................................................................................... 43.92ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker:worker_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny#","depth":4,"value":"Last login: Sat Apr 11 12:38:35 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan automated-reports 265\n\n \n No arguments expected for \"automated-reports\" command, got \"265\". \n \n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 41.20ms DONE\n cache ............................................................................................................................... 61.54ms DONE\n compiled ............................................................................................................................. 3.39ms DONE\n events ............................................................................................................................... 5.01ms DONE\n routes ............................................................................................................................... 3.37ms DONE\n views ............................................................................................................................... 43.92ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker:worker_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.12291667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.12708333,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.24583334,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.25,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ec2-user@ip-10-30-93-249:~ (nc)","depth":2,"bounds":{"left":0.36875,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.37291667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49166667,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.6145833,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.61875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7375,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7416667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"✳ Unable to access screenpipe activity data (claude)","depth":2,"bounds":{"left":0.86041665,"top":0.05888889,"width":0.12291667,"height":0.026666667},"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8645833,"top":0.06333333,"width":0.011111111,"height":0.017777778},"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.46875,"top":0.033333335,"width":0.0625,"height":0.017777778},"role_description":"text"}]...
|
-3276818620804590007
|
405592464249055517
|
visual_change
|
accessibility
|
NULL
|
Last login: Sat Apr 11 12:38:35 on ttys006
Poetry Last login: Sat Apr 11 12:38:35 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-18909-automated-reports-ask-jiminny) $ dev
root@docker_lamp_1:/home/jiminny# php artisan automated-reports 265
No arguments expected for "automated-reports" command, got "265".
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 41.20ms DONE
cache [PASSWORD_DOTS] 61.54ms DONE
compiled [PASSWORD_DOTS] 3.39ms DONE
events [PASSWORD_DOTS] 5.01ms DONE
routes [PASSWORD_DOTS] 3.37ms DONE
views [PASSWORD_DOTS] 43.92ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-emails:worker-emails_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker:worker_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
ec2-user@ip-10-30-93-249:~ (nc)
Close Tab
-zsh
Close Tab
-zsh
Close Tab
-zsh
Close Tab
✳ Unable to access screenpipe activity data (claude)
Close Tab
⌥⌘1
DEV (docker)...
|
11241
|