refactor: extract HaversineQuery helper, fix LL bind quirk
The 5 haversine SQL fragments duplicated across StationController and NationalFuelPredictionService disagreed on float-clamping (LEAST only, GREATEST/LEAST, vs. CASE WHEN). Centralised in App\Services\HaversineQuery with the safe GREATEST(-1.0, LEAST(1.0, …)) form everywhere. withinKm() embeds the radius as a numeric literal (sprintf %F) because PDO + SQLite binds float parameters as strings by default, which breaks numeric comparison against the haversine expression — a NULL filter would silently match all rows. Coordinates remain bound positionally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -103,10 +103,12 @@ class NationalFuelPredictionService
|
||||
private function getCurrentAverage(FuelType $fuelType, ?float $lat, ?float $lng): float
|
||||
{
|
||||
if ($lat !== null && $lng !== null) {
|
||||
[$radiusSql, $radiusBindings] = HaversineQuery::withinKm($lat, $lng, 50);
|
||||
|
||||
$avg = DB::table('station_prices_current')
|
||||
->join('stations', 'station_prices_current.station_id', '=', 'stations.node_id')
|
||||
->where('station_prices_current.fuel_type', $fuelType->value)
|
||||
->whereRaw('(6371 * acos(LEAST(1.0, cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) + sin(radians(?)) * sin(radians(lat))))) <= 50', [$lat, $lng, $lat])
|
||||
->whereRaw($radiusSql, $radiusBindings)
|
||||
->avg('station_prices_current.price_pence');
|
||||
|
||||
if ($avg !== null) {
|
||||
@@ -392,11 +394,13 @@ class NationalFuelPredictionService
|
||||
private function computeRegionalMomentumSignal(FuelType $fuelType, float $lat, float $lng): array
|
||||
{
|
||||
// Regional momentum: compare trend of stations within 50km vs national trend
|
||||
[$radiusSql, $radiusBindings] = HaversineQuery::withinKm($lat, $lng, 50);
|
||||
|
||||
$rows = DB::table('station_prices')
|
||||
->join('stations', 'station_prices.station_id', '=', 'stations.node_id')
|
||||
->where('station_prices.fuel_type', $fuelType->value)
|
||||
->where('station_prices.price_effective_at', '>=', now()->subDays(14))
|
||||
->whereRaw('(6371 * acos(CASE WHEN (cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) + sin(radians(?)) * sin(radians(lat))) > 1.0 THEN 1.0 ELSE (cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) + sin(radians(?)) * sin(radians(lat))) END)) <= 50', [$lat, $lng, $lat, $lat, $lng, $lat])
|
||||
->whereRaw($radiusSql, $radiusBindings)
|
||||
->selectRaw('DATE(station_prices.price_effective_at) as day, AVG(station_prices.price_pence) as avg_price')
|
||||
->groupBy('day')
|
||||
->orderBy('day')
|
||||
@@ -668,11 +672,13 @@ class NationalFuelPredictionService
|
||||
$dateString = $date->toDateString();
|
||||
|
||||
if ($lat !== null && $lng !== null) {
|
||||
[$radiusSql, $radiusBindings] = HaversineQuery::withinKm($lat, $lng, 50);
|
||||
|
||||
$regional = DB::table('station_prices')
|
||||
->join('stations', 'station_prices.station_id', '=', 'stations.node_id')
|
||||
->where('station_prices.fuel_type', $fuelType->value)
|
||||
->whereDate('station_prices.price_effective_at', $dateString)
|
||||
->whereRaw('(6371 * acos(LEAST(1.0, cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) + sin(radians(?)) * sin(radians(lat))))) <= 50', [$lat, $lng, $lat])
|
||||
->whereRaw($radiusSql, $radiusBindings)
|
||||
->avg('station_prices.price_pence');
|
||||
|
||||
if ($regional !== null) {
|
||||
@@ -697,11 +703,13 @@ class NationalFuelPredictionService
|
||||
$usedRegional = false;
|
||||
|
||||
if ($lat !== null && $lng !== null) {
|
||||
[$radiusSql, $radiusBindings] = HaversineQuery::withinKm($lat, $lng, 50);
|
||||
|
||||
$rows = DB::table('station_prices')
|
||||
->join('stations', 'station_prices.station_id', '=', 'stations.node_id')
|
||||
->where('station_prices.fuel_type', $fuelType->value)
|
||||
->where('station_prices.price_effective_at', '>=', now()->subDays($days)->startOfDay())
|
||||
->whereRaw('(6371 * acos(LEAST(1.0, cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) + sin(radians(?)) * sin(radians(lat))))) <= 50', [$lat, $lng, $lat])
|
||||
->whereRaw($radiusSql, $radiusBindings)
|
||||
->selectRaw('DATE(station_prices.price_effective_at) as day, AVG(station_prices.price_pence) as avg_price')
|
||||
->groupBy('day')
|
||||
->orderBy('day')
|
||||
|
||||
Reference in New Issue
Block a user