feat: FuelPriceService with OAuth token caching

Also extend Pest TestCase to Unit tests and guard MySQL-only migration
DDL (composite PK + PARTITION BY) behind a driver check so in-memory
SQLite tests can run migrations cleanly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ovidiu U
2026-04-03 18:52:40 +01:00
parent 80a8a9f93b
commit a83d06d76a
4 changed files with 107 additions and 33 deletions

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class FuelPriceService
{
private const TOKEN_CACHE_KEY = 'fuel_finder_access_token';
public function __construct(
private readonly StationTaggingService $taggingService,
) {}
public function getAccessToken(): string
{
return Cache::remember(self::TOKEN_CACHE_KEY, 3540, function (): string {
$response = Http::timeout(10)
->post(config('services.fuel_finder.base_url').'/oauth/generate_access_token', [
'client_id' => config('services.fuel_finder.client_id'),
'client_secret' => config('services.fuel_finder.client_secret'),
]);
return $response->json('data.access_token');
});
}
}

View File

@@ -12,7 +12,9 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('station_prices', function (Blueprint $table): void { $isMysql = DB::getDriverName() === 'mysql';
Schema::create('station_prices', function (Blueprint $table) use ($isMysql): void {
$table->bigIncrements('id'); $table->bigIncrements('id');
$table->string('station_id', 64); $table->string('station_id', 64);
$table->string('fuel_type', 20); $table->string('fuel_type', 20);
@@ -21,41 +23,45 @@ return new class extends Migration
$table->dateTime('price_reported_at'); $table->dateTime('price_reported_at');
$table->dateTime('recorded_at'); $table->dateTime('recorded_at');
// Composite PK required for MySQL range partitioning // Composite PK required for MySQL range partitioning (not supported by SQLite)
$table->primary(['id', 'price_effective_at']); if ($isMysql) {
$table->primary(['id', 'price_effective_at']);
}
$table->index(['station_id', 'fuel_type', 'price_effective_at']); $table->index(['station_id', 'fuel_type', 'price_effective_at']);
$table->index('price_effective_at'); $table->index('price_effective_at');
}); });
// Monthly partitions 20262027 + MAXVALUE catch-all // Monthly partitions 20262027 + MAXVALUE catch-all (MySQL only)
DB::statement("ALTER TABLE station_prices if ($isMysql) {
PARTITION BY RANGE (YEAR(price_effective_at) * 100 + MONTH(price_effective_at)) ( DB::statement("ALTER TABLE station_prices
PARTITION p202601 VALUES LESS THAN (202602), PARTITION BY RANGE (YEAR(price_effective_at) * 100 + MONTH(price_effective_at)) (
PARTITION p202602 VALUES LESS THAN (202603), PARTITION p202601 VALUES LESS THAN (202602),
PARTITION p202603 VALUES LESS THAN (202604), PARTITION p202602 VALUES LESS THAN (202603),
PARTITION p202604 VALUES LESS THAN (202605), PARTITION p202603 VALUES LESS THAN (202604),
PARTITION p202605 VALUES LESS THAN (202606), PARTITION p202604 VALUES LESS THAN (202605),
PARTITION p202606 VALUES LESS THAN (202607), PARTITION p202605 VALUES LESS THAN (202606),
PARTITION p202607 VALUES LESS THAN (202608), PARTITION p202606 VALUES LESS THAN (202607),
PARTITION p202608 VALUES LESS THAN (202609), PARTITION p202607 VALUES LESS THAN (202608),
PARTITION p202609 VALUES LESS THAN (202610), PARTITION p202608 VALUES LESS THAN (202609),
PARTITION p202610 VALUES LESS THAN (202611), PARTITION p202609 VALUES LESS THAN (202610),
PARTITION p202611 VALUES LESS THAN (202612), PARTITION p202610 VALUES LESS THAN (202611),
PARTITION p202612 VALUES LESS THAN (202701), PARTITION p202611 VALUES LESS THAN (202612),
PARTITION p202701 VALUES LESS THAN (202702), PARTITION p202612 VALUES LESS THAN (202701),
PARTITION p202702 VALUES LESS THAN (202703), PARTITION p202701 VALUES LESS THAN (202702),
PARTITION p202703 VALUES LESS THAN (202704), PARTITION p202702 VALUES LESS THAN (202703),
PARTITION p202704 VALUES LESS THAN (202705), PARTITION p202703 VALUES LESS THAN (202704),
PARTITION p202705 VALUES LESS THAN (202706), PARTITION p202704 VALUES LESS THAN (202705),
PARTITION p202706 VALUES LESS THAN (202707), PARTITION p202705 VALUES LESS THAN (202706),
PARTITION p202707 VALUES LESS THAN (202708), PARTITION p202706 VALUES LESS THAN (202707),
PARTITION p202708 VALUES LESS THAN (202709), PARTITION p202707 VALUES LESS THAN (202708),
PARTITION p202709 VALUES LESS THAN (202710), PARTITION p202708 VALUES LESS THAN (202709),
PARTITION p202710 VALUES LESS THAN (202711), PARTITION p202709 VALUES LESS THAN (202710),
PARTITION p202711 VALUES LESS THAN (202712), PARTITION p202710 VALUES LESS THAN (202711),
PARTITION p202712 VALUES LESS THAN (202801), PARTITION p202711 VALUES LESS THAN (202712),
PARTITION pFuture VALUES LESS THAN MAXVALUE PARTITION p202712 VALUES LESS THAN (202801),
)"); PARTITION pFuture VALUES LESS THAN MAXVALUE
)");
}
} }
/** /**

View File

@@ -16,7 +16,7 @@ use Tests\TestCase;
pest()->extend(TestCase::class) pest()->extend(TestCase::class)
// ->use(RefreshDatabase::class) // ->use(RefreshDatabase::class)
->in('Feature'); ->in('Feature', 'Unit');
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@@ -0,0 +1,40 @@
<?php
use App\Services\FuelPriceService;
use App\Services\StationTaggingService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
uses(RefreshDatabase::class);
beforeEach(function (): void {
$this->service = new FuelPriceService(new StationTaggingService());
});
it('fetches and caches an access token', function (): void {
Http::fake([
'*/oauth/generate_access_token' => Http::response([
'data' => [
'access_token' => 'test-token-abc',
'expires_in' => 3600,
],
]),
]);
$token = $this->service->getAccessToken();
expect($token)->toBe('test-token-abc');
expect(Cache::get('fuel_finder_access_token'))->toBe('test-token-abc');
});
it('returns cached token without hitting API', function (): void {
Cache::put('fuel_finder_access_token', 'cached-token', 3540);
Http::fake();
$token = $this->service->getAccessToken();
expect($token)->toBe('cached-token');
Http::assertNothingSent();
});