Remove prediction API endpoint and integrate into stations search
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

Consolidate prediction functionality by merging /api/prediction endpoint into /api/stations response. Move prediction logic from PredictionController into StationController, returning prediction data alongside station results. Replace usePrediction composable with unified useStations that returns {stations, meta, prediction}. Remove PredictionRequest, related tests, and unused Vue components (FuelFinderTest, MapTest, RecommendationTest, StationListTest). Add PredictionFull component and UpsellBanner. Extend NationalFuelPredictionService to include weekly_summary (7-day series, yesterday/today averages, cheapest/priciest days) and oil signal from price_predictions table. Update Home.vue to consume prediction from stations response. Add Plan::resolveCadenceForUser helper and configure Cashier to use custom Subscription model.
This commit is contained in:
Ovidiu U
2026-04-29 13:28:33 +01:00
parent ee6de23709
commit 088fd11058
29 changed files with 1046 additions and 499 deletions

View File

@@ -1,8 +1,10 @@
<?php
use App\Enums\FuelType;
use App\Filament\Resources\UserResource;
use App\Models\Station;
use App\Models\StationPriceCurrent;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
@@ -12,6 +14,15 @@ beforeEach(function () {
$this->withHeaders(['X-Api-Key' => config('app.api_secret_key')]);
});
function asPaidUserOnStations(string $tier = 'plus'): User
{
test()->artisan('db:seed', ['--class' => 'PlanSeeder']);
$user = User::factory()->create();
UserResource::applyTier($user, $tier, 'monthly');
return $user;
}
it('returns stations near coordinates filtered by fuel type', function () {
$station = Station::factory()->create(['lat' => 52.555064, 'lng' => -0.256119]);
StationPriceCurrent::factory()->create([
@@ -192,3 +203,37 @@ it('includes resolved lat and lng in meta when postcode is provided', function (
->assertJsonPath('meta.lat', 51.5010)
->assertJsonPath('meta.lng', -0.1415);
});
it('embeds a tier-locked prediction teaser for guest requests', function () {
Station::factory()->create(['lat' => 52.555, 'lng' => -0.256]);
$this->getJson('/api/stations?lat=52.555&lng=-0.256&fuel_type=e10&radius=10')
->assertOk()
->assertJsonPath('prediction.tier_locked', true)
->assertJsonStructure(['prediction' => ['fuel_type', 'predicted_direction', 'tier_locked']])
->assertJsonMissingPath('prediction.signals')
->assertJsonMissingPath('prediction.weekly_summary');
});
it('embeds a tier-locked teaser for free-tier authenticated users', function () {
asPaidUserOnStations('free');
$user = User::query()->latest('id')->first();
Station::factory()->create(['lat' => 52.555, 'lng' => -0.256]);
$this->actingAs($user, 'sanctum')
->getJson('/api/stations?lat=52.555&lng=-0.256&fuel_type=e10&radius=10')
->assertOk()
->assertJsonPath('prediction.tier_locked', true)
->assertJsonMissingPath('prediction.signals');
});
it('embeds the full prediction payload for plus users', function () {
$user = asPaidUserOnStations('plus');
Station::factory()->create(['lat' => 52.555, 'lng' => -0.256]);
$this->actingAs($user, 'sanctum')
->getJson('/api/stations?lat=52.555&lng=-0.256&fuel_type=e10&radius=10')
->assertOk()
->assertJsonStructure(['prediction' => ['fuel_type', 'predicted_direction', 'confidence_score', 'reasoning', 'weekly_summary', 'signals']])
->assertJsonMissingPath('prediction.tier_locked');
});