validate([ 'from' => ['nullable', 'date_format:Y-m-d'], 'to' => ['nullable', 'date_format:Y-m-d'], 'limit' => ['nullable', 'integer', 'min:1', 'max:1000'], ]); $from = (string) ($validated['from'] ?? now()->subDays(29)->toDateString()); $to = (string) ($validated['to'] ?? now()->toDateString()); $limit = (int) ($validated['limit'] ?? 100); if ($from > $to) { return response()->json([ 'message' => 'Invalid date range: from must be before or equal to to.', ], Response::HTTP_UNPROCESSABLE_ENTITY); } $rows = DB::table('feed_daily_metrics') ->selectRaw('algo_version, source') ->selectRaw('SUM(impressions) as impressions') ->selectRaw('SUM(clicks) as clicks') ->selectRaw('SUM(saves) as saves') ->selectRaw('SUM(dwell_0_5) as dwell_0_5') ->selectRaw('SUM(dwell_5_30) as dwell_5_30') ->selectRaw('SUM(dwell_30_120) as dwell_30_120') ->selectRaw('SUM(dwell_120_plus) as dwell_120_plus') ->whereBetween('metric_date', [$from, $to]) ->groupBy('algo_version', 'source') ->orderBy('algo_version') ->orderBy('source') ->get(); $byAlgoSource = $rows->map(static function ($row): array { $impressions = (int) ($row->impressions ?? 0); $clicks = (int) ($row->clicks ?? 0); $saves = (int) ($row->saves ?? 0); return [ 'algo_version' => (string) $row->algo_version, 'source' => (string) $row->source, 'impressions' => $impressions, 'clicks' => $clicks, 'saves' => $saves, 'ctr' => round($impressions > 0 ? $clicks / $impressions : 0.0, 6), 'save_rate' => round($clicks > 0 ? $saves / $clicks : 0.0, 6), 'dwell_buckets' => [ '0_5' => (int) ($row->dwell_0_5 ?? 0), '5_30' => (int) ($row->dwell_5_30 ?? 0), '30_120' => (int) ($row->dwell_30_120 ?? 0), '120_plus' => (int) ($row->dwell_120_plus ?? 0), ], ]; })->values(); $topClickedArtworks = DB::table('feed_events as e') ->leftJoin('artworks as a', 'a.id', '=', 'e.artwork_id') ->selectRaw('e.algo_version') ->selectRaw('e.source') ->selectRaw('e.artwork_id') ->selectRaw('a.title as artwork_title') ->selectRaw("SUM(CASE WHEN e.event_type = 'feed_impression' THEN 1 ELSE 0 END) AS impressions") ->selectRaw("SUM(CASE WHEN e.event_type = 'feed_click' THEN 1 ELSE 0 END) AS clicks") ->whereBetween('e.event_date', [$from, $to]) ->groupBy('e.algo_version', 'e.source', 'e.artwork_id', 'a.title') ->get() ->map(static function ($row): array { $impressions = (int) ($row->impressions ?? 0); $clicks = (int) ($row->clicks ?? 0); return [ 'algo_version' => (string) $row->algo_version, 'source' => (string) $row->source, 'artwork_id' => (int) $row->artwork_id, 'artwork_title' => (string) ($row->artwork_title ?? ''), 'impressions' => $impressions, 'clicks' => $clicks, 'ctr' => round($impressions > 0 ? $clicks / $impressions : 0.0, 6), ]; }) ->sort(static function (array $a, array $b): int { $clickCompare = $b['clicks'] <=> $a['clicks']; if ($clickCompare !== 0) { return $clickCompare; } return $b['ctr'] <=> $a['ctr']; }) ->take($limit) ->values(); return response()->json([ 'meta' => [ 'from' => $from, 'to' => $to, 'generated_at' => now()->toISOString(), 'limit' => $limit, ], 'by_algo_source' => $byAlgoSource, 'top_clicked_artworks' => $topClickedArtworks, ], Response::HTTP_OK); } }