diff --git a/app/Services/FuelPriceService.php b/app/Services/FuelPriceService.php new file mode 100644 index 0000000..5b34d6e --- /dev/null +++ b/app/Services/FuelPriceService.php @@ -0,0 +1,28 @@ +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'); + }); + } +} diff --git a/database/migrations/2026_04_03_172841_create_station_prices_table.php b/database/migrations/2026_04_03_172841_create_station_prices_table.php index 164614a..e250199 100644 --- a/database/migrations/2026_04_03_172841_create_station_prices_table.php +++ b/database/migrations/2026_04_03_172841_create_station_prices_table.php @@ -12,7 +12,9 @@ return new class extends Migration */ 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->string('station_id', 64); $table->string('fuel_type', 20); @@ -21,41 +23,45 @@ return new class extends Migration $table->dateTime('price_reported_at'); $table->dateTime('recorded_at'); - // Composite PK required for MySQL range partitioning - $table->primary(['id', 'price_effective_at']); + // Composite PK required for MySQL range partitioning (not supported by SQLite) + if ($isMysql) { + $table->primary(['id', 'price_effective_at']); + } $table->index(['station_id', 'fuel_type', 'price_effective_at']); $table->index('price_effective_at'); }); - // Monthly partitions 2026–2027 + MAXVALUE catch-all - DB::statement("ALTER TABLE station_prices - PARTITION BY RANGE (YEAR(price_effective_at) * 100 + MONTH(price_effective_at)) ( - PARTITION p202601 VALUES LESS THAN (202602), - PARTITION p202602 VALUES LESS THAN (202603), - PARTITION p202603 VALUES LESS THAN (202604), - PARTITION p202604 VALUES LESS THAN (202605), - PARTITION p202605 VALUES LESS THAN (202606), - PARTITION p202606 VALUES LESS THAN (202607), - PARTITION p202607 VALUES LESS THAN (202608), - PARTITION p202608 VALUES LESS THAN (202609), - PARTITION p202609 VALUES LESS THAN (202610), - PARTITION p202610 VALUES LESS THAN (202611), - PARTITION p202611 VALUES LESS THAN (202612), - PARTITION p202612 VALUES LESS THAN (202701), - PARTITION p202701 VALUES LESS THAN (202702), - PARTITION p202702 VALUES LESS THAN (202703), - PARTITION p202703 VALUES LESS THAN (202704), - PARTITION p202704 VALUES LESS THAN (202705), - PARTITION p202705 VALUES LESS THAN (202706), - PARTITION p202706 VALUES LESS THAN (202707), - PARTITION p202707 VALUES LESS THAN (202708), - PARTITION p202708 VALUES LESS THAN (202709), - PARTITION p202709 VALUES LESS THAN (202710), - PARTITION p202710 VALUES LESS THAN (202711), - PARTITION p202711 VALUES LESS THAN (202712), - PARTITION p202712 VALUES LESS THAN (202801), - PARTITION pFuture VALUES LESS THAN MAXVALUE - )"); + // Monthly partitions 2026–2027 + MAXVALUE catch-all (MySQL only) + if ($isMysql) { + DB::statement("ALTER TABLE station_prices + PARTITION BY RANGE (YEAR(price_effective_at) * 100 + MONTH(price_effective_at)) ( + PARTITION p202601 VALUES LESS THAN (202602), + PARTITION p202602 VALUES LESS THAN (202603), + PARTITION p202603 VALUES LESS THAN (202604), + PARTITION p202604 VALUES LESS THAN (202605), + PARTITION p202605 VALUES LESS THAN (202606), + PARTITION p202606 VALUES LESS THAN (202607), + PARTITION p202607 VALUES LESS THAN (202608), + PARTITION p202608 VALUES LESS THAN (202609), + PARTITION p202609 VALUES LESS THAN (202610), + PARTITION p202610 VALUES LESS THAN (202611), + PARTITION p202611 VALUES LESS THAN (202612), + PARTITION p202612 VALUES LESS THAN (202701), + PARTITION p202701 VALUES LESS THAN (202702), + PARTITION p202702 VALUES LESS THAN (202703), + PARTITION p202703 VALUES LESS THAN (202704), + PARTITION p202704 VALUES LESS THAN (202705), + PARTITION p202705 VALUES LESS THAN (202706), + PARTITION p202706 VALUES LESS THAN (202707), + PARTITION p202707 VALUES LESS THAN (202708), + PARTITION p202708 VALUES LESS THAN (202709), + PARTITION p202709 VALUES LESS THAN (202710), + PARTITION p202710 VALUES LESS THAN (202711), + PARTITION p202711 VALUES LESS THAN (202712), + PARTITION p202712 VALUES LESS THAN (202801), + PARTITION pFuture VALUES LESS THAN MAXVALUE + )"); + } } /** diff --git a/tests/Pest.php b/tests/Pest.php index cb09b7f..6cec88e 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -16,7 +16,7 @@ use Tests\TestCase; pest()->extend(TestCase::class) // ->use(RefreshDatabase::class) - ->in('Feature'); + ->in('Feature', 'Unit'); /* |-------------------------------------------------------------------------- diff --git a/tests/Unit/Services/FuelPriceServiceTest.php b/tests/Unit/Services/FuelPriceServiceTest.php new file mode 100644 index 0000000..169f909 --- /dev/null +++ b/tests/Unit/Services/FuelPriceServiceTest.php @@ -0,0 +1,40 @@ +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(); +});