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); } $byAlgoRows = DB::table('similar_artwork_events') ->selectRaw('algo_version') ->selectRaw("SUM(CASE WHEN event_type = 'impression' THEN 1 ELSE 0 END) AS impressions") ->selectRaw("SUM(CASE WHEN event_type = 'click' THEN 1 ELSE 0 END) AS clicks") ->whereBetween('event_date', [$from, $to]) ->groupBy('algo_version') ->orderBy('algo_version') ->get(); $byAlgo = $byAlgoRows->map(static function ($row): array { $impressions = (int) ($row->impressions ?? 0); $clicks = (int) ($row->clicks ?? 0); $ctr = $impressions > 0 ? $clicks / $impressions : 0.0; return [ 'algo_version' => (string) $row->algo_version, 'impressions' => $impressions, 'clicks' => $clicks, 'ctr' => round($ctr, 6), ]; })->values(); $pairRows = DB::table('similar_artwork_events as e') ->leftJoin('artworks as source', 'source.id', '=', 'e.source_artwork_id') ->leftJoin('artworks as similar', 'similar.id', '=', 'e.similar_artwork_id') ->selectRaw('e.algo_version') ->selectRaw('e.source_artwork_id') ->selectRaw('e.similar_artwork_id') ->selectRaw('source.title as source_title') ->selectRaw('similar.title as similar_title') ->selectRaw("SUM(CASE WHEN e.event_type = 'impression' THEN 1 ELSE 0 END) AS impressions") ->selectRaw("SUM(CASE WHEN e.event_type = 'click' THEN 1 ELSE 0 END) AS clicks") ->whereBetween('e.event_date', [$from, $to]) ->whereNotNull('e.similar_artwork_id') ->groupBy('e.algo_version', 'e.source_artwork_id', 'e.similar_artwork_id', 'source.title', 'similar.title') ->get(); $topSimilarities = $pairRows ->map(static function ($row): array { $impressions = (int) ($row->impressions ?? 0); $clicks = (int) ($row->clicks ?? 0); $ctr = $impressions > 0 ? $clicks / $impressions : 0.0; return [ 'algo_version' => (string) $row->algo_version, 'source_artwork_id' => (int) $row->source_artwork_id, 'source_title' => (string) ($row->source_title ?? ''), 'similar_artwork_id' => (int) $row->similar_artwork_id, 'similar_title' => (string) ($row->similar_title ?? ''), 'impressions' => $impressions, 'clicks' => $clicks, 'ctr' => round($ctr, 6), ]; }) ->sort(function (array $a, array $b): int { $ctrCompare = $b['ctr'] <=> $a['ctr']; if ($ctrCompare !== 0) { return $ctrCompare; } $clickCompare = $b['clicks'] <=> $a['clicks']; if ($clickCompare !== 0) { return $clickCompare; } return $b['impressions'] <=> $a['impressions']; }) ->take($limit) ->values(); return response()->json([ 'meta' => [ 'from' => $from, 'to' => $to, 'generated_at' => now()->toISOString(), 'limit' => $limit, ], 'by_algo_version' => $byAlgo, 'top_similarities' => $topSimilarities, ], Response::HTTP_OK); } }