= 80 (same threshold as the ingestion discard). * * Schedule: daily at 02:00 UTC (see bootstrap/app.php →withSchedule). * Manual backfill: php artisan mdn:analytics:rollup-page-views --date=2026-07-10 */ class RollupAnalyticsPageViews extends Command { protected $signature = 'mdn:analytics:rollup-page-views {--date= : Date to aggregate (YYYY-MM-DD). Defaults to yesterday.}'; protected $description = 'Roll up page_view events into analytics_daily_page_views.'; public function __construct(private readonly UuidGenerator $ids) { parent::__construct(); } public function handle(): int { $date = $this->option('date') ? Carbon::parse($this->option('date'))->toDateString() : Carbon::yesterday()->toDateString(); $this->line("Aggregating page views for {$date} …"); $rows = DB::table('analytics_events') ->selectRaw(" DATE(occurred_at) AS summary_date, COALESCE(path, '/') AS path, module, COUNT(*) AS page_views, COUNT(DISTINCT visitor_id) AS unique_visitors, COUNT(DISTINCT session_id) AS sessions ") ->where('event_type', 'page_view') ->where('bot_score', '<', 80) ->whereDate('occurred_at', $date) ->groupByRaw("DATE(occurred_at), COALESCE(path, '/'), module") ->get(); if ($rows->isEmpty()) { $this->line(" No page_view events for {$date} — nothing to write."); return self::SUCCESS; } $now = Carbon::now()->toDateTimeString(); $upsertRows = $rows->map(fn (object $r) => [ 'id' => $this->ids->generate(), 'summary_date' => $r->summary_date, 'path' => $r->path, 'module' => $r->module, 'page_views' => (int) $r->page_views, 'unique_visitors' => (int) $r->unique_visitors, 'sessions' => (int) $r->sessions, 'created_at' => $now, 'updated_at' => $now, ])->all(); DB::table('analytics_daily_page_views')->upsert( $upsertRows, ['summary_date', 'path'], ['page_views', 'unique_visitors', 'sessions', 'updated_at'], ); $this->info(" Done. {$rows->count()} path/module combinations written for {$date}."); return self::SUCCESS; } }