where('fuel_type', $context->fuelType->value) ->where('price_effective_at', '>=', now()->subDays(90)) ->selectRaw("{$dowExpr} as dow, DATE(price_effective_at) as day, AVG(price_pence) as avg_price") ->groupBy('dow', 'day') ->get(); $uniqueDays = $rows->pluck('day')->unique()->count(); if ($uniqueDays < self::MIN_DAYS) { return $this->disabledSignal("Insufficient history for day-of-week pattern ({$uniqueDays} days, need ".self::MIN_DAYS.')'); } $dowAverages = $rows->groupBy('dow')->map(fn ($g) => $g->avg('avg_price')); $weekAvg = $dowAverages->avg(); $todayDow = (int) now()->format('w') + 1; // PHP 0=Sun → MySQL 1=Sun $todayAvg = $dowAverages->get($todayDow, $weekAvg); $cheapestDow = $dowAverages->keys()->sortBy(fn ($k) => $dowAverages[$k])->first(); $dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; $todayName = $dayNames[($todayDow - 1) % 7] ?? 'Today'; $tomorrowName = $dayNames[$todayDow % 7] ?? 'Tomorrow'; $todayDeltaPence = round(($todayAvg - $weekAvg) / 100, 1); $tomorrowDeltaPence = round(($dowAverages->get(($todayDow % 7) + 1, $weekAvg) - $todayAvg) / 100, 1); $direction = match (true) { ($todayAvg - $weekAvg) / 100 >= 1.5 => 'up', ($weekAvg - $todayAvg) / 100 >= 1.5 => 'down', default => 'stable', }; $score = $direction === 'stable' ? 0.0 : ($direction === 'up' ? 1.0 : -1.0); $parts = []; $parts[] = abs($todayDeltaPence) < 0.1 ? "Today ({$todayName}) is typically in line with the weekly average." : sprintf( 'Today (%s) is typically %sp %s the weekly average.', $todayName, number_format(abs($todayDeltaPence), 1), $todayDeltaPence > 0 ? 'above' : 'below', ); $parts[] = abs($tomorrowDeltaPence) < 0.1 ? "Tomorrow ({$tomorrowName}) is typically the same." : sprintf( 'Tomorrow (%s) is typically %sp %s.', $tomorrowName, number_format(abs($tomorrowDeltaPence), 1), $tomorrowDeltaPence < 0 ? 'cheaper' : 'pricier', ); if ($cheapestDow === $todayDow) { $parts[] = 'Today is historically the cheapest day of the week.'; } return [ 'score' => $score, 'confidence' => min(1.0, $uniqueDays / 90), 'direction' => $direction, 'detail' => implode(' ', $parts), 'data_points' => $uniqueDays, 'enabled' => true, ]; } }