chore: audit nits — PlanFeatures, test boot, EIA log, static method

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) <noreply@anthropic.com>
This commit is contained in:
Ovidiu U
2026-04-29 20:00:09 +01:00
parent 7f64c42a23
commit c46b017b51
6 changed files with 27 additions and 13 deletions

View File

@@ -7,6 +7,7 @@ use App\Services\BrentPriceSources\BrentPriceFetchException;
use Illuminate\Console\Attributes\Description; use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature; use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
#[Signature('oil:fetch')] #[Signature('oil:fetch')]
#[Description('Fetch latest Brent crude prices (EIA primary, FRED fallback)')] #[Description('Fetch latest Brent crude prices (EIA primary, FRED fallback)')]
@@ -20,6 +21,7 @@ class FetchOilPrices extends Command
return self::SUCCESS; return self::SUCCESS;
} catch (BrentPriceFetchException $e) { } 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...'); $this->warn('EIA fetch failed: '.$e->getMessage().'. Trying FRED...');
} }
@@ -29,6 +31,7 @@ class FetchOilPrices extends Command
return self::SUCCESS; return self::SUCCESS;
} catch (BrentPriceFetchException $e) { } catch (BrentPriceFetchException $e) {
Log::error('FetchOilPrices: both EIA and FRED failed', ['error' => $e->getMessage()]);
$this->error('Both EIA and FRED failed: '.$e->getMessage()); $this->error('Both EIA and FRED failed: '.$e->getMessage());
return self::FAILURE; return self::FAILURE;

View File

@@ -9,6 +9,7 @@ use App\Filament\Resources\UserResource\Pages\EditUser;
use App\Filament\Resources\UserResource\Pages\ListUsers; use App\Filament\Resources\UserResource\Pages\ListUsers;
use App\Models\Plan; use App\Models\Plan;
use App\Models\User; use App\Models\User;
use App\Services\PlanFeatures;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\DateTimePicker;
@@ -75,7 +76,7 @@ class UserResource extends Resource
->live() ->live()
->dehydrated(false) ->dehydrated(false)
->afterStateHydrated(fn (Select $component, ?User $record) => $component ->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') Select::make('cadence')
->label('Billing Cadence') ->label('Billing Cadence')
->options([ ->options([
@@ -131,7 +132,7 @@ class UserResource extends Resource
TextColumn::make('postcode')->placeholder('—'), TextColumn::make('postcode')->placeholder('—'),
TextColumn::make('tier') TextColumn::make('tier')
->label('Tier') ->label('Tier')
->state(fn (User $record): string => Plan::resolveForUser($record)->name) ->state(fn (User $record): string => PlanFeatures::for($record)->tier())
->badge() ->badge()
->colors([ ->colors([
'gray' => 'free', 'gray' => 'free',

View File

@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Plan; use App\Models\Plan;
use App\Models\User; use App\Models\User;
use App\Services\PlanFeatures;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -70,7 +71,7 @@ class AuthController extends Controller
return response()->json(array_merge( return response()->json(array_merge(
$user->toArray(), $user->toArray(),
[ [
'tier' => Plan::resolveForUser($user)->name, 'tier' => PlanFeatures::for($user)->tier(),
'subscription_cancelled' => $subscription?->canceled() ?? false, 'subscription_cancelled' => $subscription?->canceled() ?? false,
'subscription_cadence' => Plan::resolveCadenceForUser($user), 'subscription_cadence' => Plan::resolveCadenceForUser($user),
'subscribed_at' => $subscription?->created_at?->toIso8601String(), 'subscribed_at' => $subscription?->created_at?->toIso8601String(),

View File

@@ -59,13 +59,6 @@ class AppServiceProvider extends ServiceProvider
app()->isProduction(), 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::defaults(fn (): ?Password => app()->isProduction()
? Password::min(12) ? Password::min(12)
->mixedCase() ->mixedCase()

View File

@@ -209,9 +209,9 @@ class FuelPriceService
'postcode' => $data['location']['postcode'], 'postcode' => $data['location']['postcode'],
'lat' => $data['location']['latitude'], 'lat' => $data['location']['latitude'],
'lng' => $data['location']['longitude'], 'lng' => $data['location']['longitude'],
'amenities' => self::flattenEnabledFlags($data['amenities'] ?? []), 'amenities' => $this->flattenEnabledFlags($data['amenities'] ?? []),
'opening_times' => $data['opening_times'] ?? null, 'opening_times' => $data['opening_times'] ?? null,
'fuel_types' => self::flattenEnabledFlags($data['fuel_types'] ?? []), 'fuel_types' => $this->flattenEnabledFlags($data['fuel_types'] ?? []),
'last_seen_at' => $now, 'last_seen_at' => $now,
]); ]);
@@ -242,7 +242,7 @@ class FuelPriceService
* @param array<string, bool>|array<int, string> $flags * @param array<string, bool>|array<int, string> $flags
* @return array<int, string> * @return array<int, string>
*/ */
private static function flattenEnabledFlags(array $flags): array private function flattenEnabledFlags(array $flags): array
{ {
if ($flags === []) { if ($flags === []) {
return []; return [];

View File

@@ -3,10 +3,26 @@
namespace Tests; namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\DB;
use Laravel\Fortify\Features; use Laravel\Fortify\Features;
abstract class TestCase extends BaseTestCase 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 protected function skipUnlessFortifyHas(string $feature, ?string $message = null): void
{ {
if (! Features::enabled($feature)) { if (! Features::enabled($feature)) {