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

@@ -80,6 +80,41 @@ class Plan extends Model
]);
}
/**
* Resolve the active subscription cadence for a user.
* Returns 'monthly' | 'annual', or null if the user has no paid subscription.
*/
public static function resolveCadenceForUser(User $user): ?string
{
if (! method_exists($user, 'subscriptions')) {
return null;
}
$priceId = $user->subscriptions()->active()->value('stripe_price');
if ($priceId === null) {
return null;
}
$plan = static::where('stripe_price_id_monthly', $priceId)
->orWhere('stripe_price_id_annual', $priceId)
->first();
if ($plan === null) {
return null;
}
if ($plan->stripe_price_id_monthly === $priceId) {
return 'monthly';
}
if ($plan->stripe_price_id_annual === $priceId) {
return 'annual';
}
return null;
}
protected static function booted(): void
{
static::saved(function (): void {

View File

@@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
#[Fillable(['predicted_for', 'source', 'direction', 'confidence', 'reasoning', 'generated_at'])]
class PricePrediction extends Model
@@ -39,11 +38,17 @@ class PricePrediction extends Model
*/
public function scopeBestFirst(Builder $query): Builder
{
$priority = implode(', ', array_map(
fn (string $v) => "'$v'",
[PredictionSource::LlmWithContext->value, PredictionSource::Llm->value, PredictionSource::Ewma->value],
));
$priority = [
PredictionSource::LlmWithContext->value,
PredictionSource::Llm->value,
PredictionSource::Ewma->value,
];
return $query->orderByRaw("FIELD(source, $priority)");
$cases = '';
foreach ($priority as $rank => $source) {
$cases .= " WHEN '$source' THEN $rank";
}
return $query->orderByRaw("CASE source$cases ELSE ".count($priority).' END');
}
}