Audit item #10. PriceReliability::fromUpdatedAt was being invoked ~10× per station per /api/stations response — twice in the sort comparator (once for $a, once for $b), once in the count groupBy, and once per resource render. PriceClassification::fromUpdatedAt was called twice inside the resource (value + label). The controller now computes the parsed datetime + reliability + classification once per row and stashes them on the row. Sort, groupBy, and StationResource read the cached values; the resource keeps a fresh-compute fallback for callers that bypass the controller. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
94 lines
3.3 KiB
PHP
94 lines
3.3 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Resources\Api;
|
|
|
|
use App\Enums\PriceClassification;
|
|
use App\Enums\PriceReliability;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Resources\Json\JsonResource;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
class StationResource extends JsonResource
|
|
{
|
|
public function toArray(Request $request): array
|
|
{
|
|
// The controller pre-computes _updated_at / _reliability / _classification
|
|
// per row. Falling back to fresh computation keeps the resource usable
|
|
// outside that path (e.g. tests or future callers).
|
|
$updatedAt = $this->_updated_at
|
|
?? ($this->price_effective_at ? Carbon::parse($this->price_effective_at) : null);
|
|
$reliability = $this->_reliability ?? PriceReliability::fromUpdatedAt($updatedAt);
|
|
$classification = $this->_classification ?? PriceClassification::fromUpdatedAt($updatedAt);
|
|
|
|
return [
|
|
'station_id' => $this->node_id,
|
|
'name' => $this->trading_name,
|
|
'brand' => $this->brand_name,
|
|
'is_supermarket' => (bool) $this->is_supermarket,
|
|
'is_motorway' => (bool) $this->is_motorway_service_station,
|
|
'address' => implode(', ', array_filter([$this->address_line_1, $this->city])),
|
|
'postcode' => $this->postcode,
|
|
'lat' => (float) $this->lat,
|
|
'lng' => (float) $this->lng,
|
|
'distance_km' => round((float) $this->distance_km, 2),
|
|
'fuel_type' => $this->fuel_type,
|
|
'fuel_types_available' => $this->fuel_types ?? [],
|
|
'amenities' => $this->amenities ?? [],
|
|
'open_today' => $this->openTodayPayload(),
|
|
'price_pence' => (int) $this->price_pence,
|
|
'price' => round((int) $this->price_pence / 100, 2),
|
|
'price_updated_at' => $updatedAt?->toISOString(),
|
|
'price_classification' => $classification->value,
|
|
'price_classification_label' => $classification->label(),
|
|
'reliability' => $reliability->value,
|
|
'reliability_label' => $reliability->label(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{is_24_hours: bool, open: ?string, close: ?string, is_open_now: bool}|null
|
|
*/
|
|
private function openTodayPayload(): ?array
|
|
{
|
|
$times = $this->opening_times;
|
|
|
|
if (! is_array($times) || empty($times['usual_days'])) {
|
|
return null;
|
|
}
|
|
|
|
$now = Carbon::now('Europe/London');
|
|
$dayKey = strtolower($now->format('l'));
|
|
$today = $times['usual_days'][$dayKey] ?? null;
|
|
|
|
if (! is_array($today)) {
|
|
return null;
|
|
}
|
|
|
|
$is24 = (bool) ($today['is_24_hours'] ?? false);
|
|
$open = $today['open'] ?? null;
|
|
$close = $today['close'] ?? null;
|
|
|
|
return [
|
|
'is_24_hours' => $is24,
|
|
'open' => $open ? substr($open, 0, 5) : null,
|
|
'close' => $close ? substr($close, 0, 5) : null,
|
|
'is_open_now' => $this->computeIsOpenNow($is24, $open, $close, $now),
|
|
];
|
|
}
|
|
|
|
private function computeIsOpenNow(bool $is24, ?string $open, ?string $close, Carbon $now): bool
|
|
{
|
|
if ($is24) {
|
|
return true;
|
|
}
|
|
|
|
if (! $open || ! $close) {
|
|
return false;
|
|
}
|
|
|
|
$current = $now->format('H:i:s');
|
|
|
|
return $current >= $open && $current < $close;
|
|
}
|
|
}
|