From c46b017b5168a3df9a9b170d23e7c17534c83a33 Mon Sep 17 00:00:00 2001 From: Ovidiu U Date: Wed, 29 Apr 2026 20:00:09 +0100 Subject: [PATCH] =?UTF-8?q?chore:=20audit=20nits=20=E2=80=94=20PlanFeature?= =?UTF-8?q?s,=20test=20boot,=20EIA=20log,=20static=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit items #15, #16, #20, #22. #15 — AuthController::me and UserResource form/table now read tier via PlanFeatures::for($user)->tier() instead of Plan::resolveForUser($user) ->name. Tiers.md: PlanFeatures is the single entitlement gate. #16 — Moved SQLite GREATEST/LEAST PHP-backed function registration from AppServiceProvider::boot to tests/TestCase::setUp. Production app boot no longer checks the DB driver name. #20 — FetchOilPrices: added Log::warning on EIA fallback and Log::error on both-providers-failed so primary-source reliability can be trended beyond the cron output buffer. #22 — FuelPriceService::flattenEnabledFlags is now an instance method, matching the rest of the class. No external callers. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/Console/Commands/FetchOilPrices.php | 3 +++ app/Filament/Resources/UserResource.php | 5 +++-- app/Http/Controllers/Api/AuthController.php | 3 ++- app/Providers/AppServiceProvider.php | 7 ------- app/Services/FuelPriceService.php | 6 +++--- tests/TestCase.php | 16 ++++++++++++++++ 6 files changed, 27 insertions(+), 13 deletions(-) diff --git a/app/Console/Commands/FetchOilPrices.php b/app/Console/Commands/FetchOilPrices.php index 7cbe695..be11547 100644 --- a/app/Console/Commands/FetchOilPrices.php +++ b/app/Console/Commands/FetchOilPrices.php @@ -7,6 +7,7 @@ use App\Services\BrentPriceSources\BrentPriceFetchException; use Illuminate\Console\Attributes\Description; use Illuminate\Console\Attributes\Signature; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Log; #[Signature('oil:fetch')] #[Description('Fetch latest Brent crude prices (EIA primary, FRED fallback)')] @@ -20,6 +21,7 @@ class FetchOilPrices extends Command return self::SUCCESS; } catch (BrentPriceFetchException $e) { + Log::warning('FetchOilPrices: EIA fetch failed, falling back to FRED', ['error' => $e->getMessage()]); $this->warn('EIA fetch failed: '.$e->getMessage().'. Trying FRED...'); } @@ -29,6 +31,7 @@ class FetchOilPrices extends Command return self::SUCCESS; } catch (BrentPriceFetchException $e) { + Log::error('FetchOilPrices: both EIA and FRED failed', ['error' => $e->getMessage()]); $this->error('Both EIA and FRED failed: '.$e->getMessage()); return self::FAILURE; diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index 1ec4ce3..ef3ffeb 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -9,6 +9,7 @@ use App\Filament\Resources\UserResource\Pages\EditUser; use App\Filament\Resources\UserResource\Pages\ListUsers; use App\Models\Plan; use App\Models\User; +use App\Services\PlanFeatures; use Filament\Actions\DeleteAction; use Filament\Actions\EditAction; use Filament\Forms\Components\DateTimePicker; @@ -75,7 +76,7 @@ class UserResource extends Resource ->live() ->dehydrated(false) ->afterStateHydrated(fn (Select $component, ?User $record) => $component - ->state($record ? Plan::resolveForUser($record)->name : PlanTier::Free->value)), + ->state($record ? PlanFeatures::for($record)->tier() : PlanTier::Free->value)), Select::make('cadence') ->label('Billing Cadence') ->options([ @@ -131,7 +132,7 @@ class UserResource extends Resource TextColumn::make('postcode')->placeholder('—'), TextColumn::make('tier') ->label('Tier') - ->state(fn (User $record): string => Plan::resolveForUser($record)->name) + ->state(fn (User $record): string => PlanFeatures::for($record)->tier()) ->badge() ->colors([ 'gray' => 'free', diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index 9740606..5a1827e 100644 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Models\Plan; use App\Models\User; +use App\Services\PlanFeatures; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -70,7 +71,7 @@ class AuthController extends Controller return response()->json(array_merge( $user->toArray(), [ - 'tier' => Plan::resolveForUser($user)->name, + 'tier' => PlanFeatures::for($user)->tier(), 'subscription_cancelled' => $subscription?->canceled() ?? false, 'subscription_cadence' => Plan::resolveCadenceForUser($user), 'subscribed_at' => $subscription?->created_at?->toIso8601String(), diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f8e6951..4279bbf 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -59,13 +59,6 @@ class AppServiceProvider extends ServiceProvider app()->isProduction(), ); - // SQLite lacks GREATEST/LEAST scalar functions — register them for tests. - if (DB::connection()->getDriverName() === 'sqlite') { - $pdo = DB::connection()->getPdo(); - $pdo->sqliteCreateFunction('GREATEST', fn (...$args) => max($args), -1); - $pdo->sqliteCreateFunction('LEAST', fn (...$args) => min($args), -1); - } - Password::defaults(fn (): ?Password => app()->isProduction() ? Password::min(12) ->mixedCase() diff --git a/app/Services/FuelPriceService.php b/app/Services/FuelPriceService.php index ad66f47..c7ab1e3 100644 --- a/app/Services/FuelPriceService.php +++ b/app/Services/FuelPriceService.php @@ -209,9 +209,9 @@ class FuelPriceService 'postcode' => $data['location']['postcode'], 'lat' => $data['location']['latitude'], 'lng' => $data['location']['longitude'], - 'amenities' => self::flattenEnabledFlags($data['amenities'] ?? []), + 'amenities' => $this->flattenEnabledFlags($data['amenities'] ?? []), 'opening_times' => $data['opening_times'] ?? null, - 'fuel_types' => self::flattenEnabledFlags($data['fuel_types'] ?? []), + 'fuel_types' => $this->flattenEnabledFlags($data['fuel_types'] ?? []), 'last_seen_at' => $now, ]); @@ -242,7 +242,7 @@ class FuelPriceService * @param array|array $flags * @return array */ - private static function flattenEnabledFlags(array $flags): array + private function flattenEnabledFlags(array $flags): array { if ($flags === []) { return []; diff --git a/tests/TestCase.php b/tests/TestCase.php index a574f37..529477b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,10 +3,26 @@ namespace Tests; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; +use Illuminate\Support\Facades\DB; use Laravel\Fortify\Features; abstract class TestCase extends BaseTestCase { + protected function setUp(): void + { + parent::setUp(); + + // SQLite lacks GREATEST/LEAST scalar functions — register PHP-backed + // shims so the haversine and other math expressions used in + // production-style queries run identically in :memory: tests. + // Idempotent: registering twice on the same PDO is harmless. + if (DB::connection()->getDriverName() === 'sqlite') { + $pdo = DB::connection()->getPdo(); + $pdo->sqliteCreateFunction('GREATEST', fn (...$args) => max($args), -1); + $pdo->sqliteCreateFunction('LEAST', fn (...$args) => min($args), -1); + } + } + protected function skipUnlessFortifyHas(string $feature, ?string $message = null): void { if (! Features::enabled($feature)) {