The 803-line NationalFuelPredictionService had six private compute*Signal
methods, a private linearRegression helper, and a private disabledSignal
shape factory all crammed together. Each signal is now an independently
testable class.
- App\Services\Prediction\Signals\Signal — interface
- App\Services\Prediction\Signals\SignalContext — input value object
(FuelType + optional lat/lng + hasCoordinates() helper)
- App\Services\Prediction\Signals\AbstractSignal — shared
disabledSignal() and linearRegression() helpers
- TrendSignal, DayOfWeekSignal, BrandBehaviourSignal, StickinessSignal,
RegionalMomentumSignal, OilSignal — one class each, extending
AbstractSignal
NationalFuelPredictionService receives the 6 signal classes via constructor
injection and orchestrates them. The lat/lng null-guard for regional
momentum now lives inside RegionalMomentumSignal::compute() so the
coordinator no longer branches on coordinate presence.
Aggregation, weekly summary, and reasoning helpers stay in the service
for now — they are coupled to the public predict() output shape and are
candidates for a follow-up extraction once a stable API is locked in.
Service: 803 → 414 lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>