feat: add NationalFuelPredictionService with trend, day-of-week, brand-behaviour, and stickiness signals
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
107
tests/Unit/Services/NationalFuelPredictionServiceTest.php
Normal file
107
tests/Unit/Services/NationalFuelPredictionServiceTest.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\FuelType;
|
||||
use App\Models\Station;
|
||||
use App\Models\StationPrice;
|
||||
use App\Models\StationPriceCurrent;
|
||||
use App\Services\NationalFuelPredictionService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('returns no_signal when there is insufficient price history', function () {
|
||||
$result = app(NationalFuelPredictionService::class)->predict(FuelType::B7Standard);
|
||||
|
||||
expect($result['predicted_direction'])->toBe('stable')
|
||||
->and($result['signals']['trend']['enabled'])->toBeFalse()
|
||||
->and($result['action'])->toBe('no_signal');
|
||||
});
|
||||
|
||||
it('detects rising trend from consistently increasing daily averages', function () {
|
||||
$station = Station::factory()->create();
|
||||
|
||||
// 7 days of prices rising at ~100 pence/day
|
||||
for ($daysAgo = 6; $daysAgo >= 0; $daysAgo--) {
|
||||
StationPrice::factory()->create([
|
||||
'station_id' => $station->node_id,
|
||||
'fuel_type' => FuelType::B7Standard,
|
||||
'price_pence' => 14000 + ((6 - $daysAgo) * 100),
|
||||
'price_effective_at' => now()->subDays($daysAgo)->setTime(12, 0),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = app(NationalFuelPredictionService::class)->predict(FuelType::B7Standard);
|
||||
|
||||
expect($result['signals']['trend']['direction'])->toBe('up')
|
||||
->and($result['signals']['trend']['enabled'])->toBeTrue()
|
||||
->and($result['predicted_direction'])->toBe('up')
|
||||
->and($result['action'])->toBe('fill_now');
|
||||
});
|
||||
|
||||
it('detects falling trend from consistently decreasing daily averages', function () {
|
||||
$station = Station::factory()->create();
|
||||
|
||||
for ($daysAgo = 6; $daysAgo >= 0; $daysAgo--) {
|
||||
StationPrice::factory()->create([
|
||||
'station_id' => $station->node_id,
|
||||
'fuel_type' => FuelType::B7Standard,
|
||||
'price_pence' => 16000 - ((6 - $daysAgo) * 100),
|
||||
'price_effective_at' => now()->subDays($daysAgo)->setTime(12, 0),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = app(NationalFuelPredictionService::class)->predict(FuelType::B7Standard);
|
||||
|
||||
expect($result['signals']['trend']['direction'])->toBe('down')
|
||||
->and($result['predicted_direction'])->toBe('down')
|
||||
->and($result['action'])->toBe('wait');
|
||||
});
|
||||
|
||||
it('returns current_avg from station_prices_current', function () {
|
||||
$station = Station::factory()->create();
|
||||
StationPriceCurrent::factory()->create([
|
||||
'station_id' => $station->node_id,
|
||||
'fuel_type' => FuelType::B7Standard,
|
||||
'price_pence' => 14750,
|
||||
]);
|
||||
|
||||
$result = app(NationalFuelPredictionService::class)->predict(FuelType::B7Standard);
|
||||
|
||||
expect($result['current_avg'])->toBe(147.5);
|
||||
});
|
||||
|
||||
it('includes all required keys in response', function () {
|
||||
$result = app(NationalFuelPredictionService::class)->predict(FuelType::B7Standard);
|
||||
|
||||
expect($result)->toHaveKeys([
|
||||
'fuel_type', 'current_avg', 'predicted_direction', 'predicted_change_pence',
|
||||
'confidence_score', 'confidence_label', 'action', 'reasoning',
|
||||
'prediction_horizon_days', 'region_key', 'methodology',
|
||||
'signals',
|
||||
]);
|
||||
|
||||
expect($result['signals'])->toHaveKeys([
|
||||
'trend', 'day_of_week', 'brand_behaviour',
|
||||
'national_momentum', 'regional_momentum', 'price_stickiness',
|
||||
]);
|
||||
});
|
||||
|
||||
it('disables trend signal when r_squared is below 0.5', function () {
|
||||
$station = Station::factory()->create();
|
||||
|
||||
// Highly erratic prices (zigzag pattern) — low R²
|
||||
$prices = [14000, 16000, 13000, 17000, 12000, 18000, 14500];
|
||||
foreach ($prices as $daysAgo => $price) {
|
||||
StationPrice::factory()->create([
|
||||
'station_id' => $station->node_id,
|
||||
'fuel_type' => FuelType::B7Standard,
|
||||
'price_pence' => $price,
|
||||
'price_effective_at' => now()->subDays(count($prices) - 1 - $daysAgo)->setTime(12, 0),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = app(NationalFuelPredictionService::class)->predict(FuelType::B7Standard);
|
||||
|
||||
// Trend signal may be disabled if both 5-day and 14-day lookbacks fail R² threshold
|
||||
expect($result['signals']['trend']['data_points'])->toBeInt();
|
||||
});
|
||||
Reference in New Issue
Block a user