Audit items #17 and #21.
#17 — DayOfWeekSignal and StickinessSignal each had their own
isSqlite ternary picking between SQLite (strftime/julianday) and
MySQL (DAYOFWEEK/DATEDIFF) date expressions. Centralised in
App\Services\Prediction\Signals\DbDialect.
#21 — ProfileValidationRules was a trait with one consumer
(CreateNewUser); inlined the rules into the action and deleted the
trait. Also dropped PasswordValidationRules::currentPasswordRules()
which was unused. PasswordValidationRules trait stays (two consumers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>