Add comprehensive project documentation and architecture guidelines
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

Establishes core rules and conventions for the FuelAlert Laravel application: architecture patterns (fat services, thin controllers), database schema with partitioned station_prices table, multi-tier notification system with Vonage and OneSignal, 4-signal scoring engine for fuel price recommendations, Stripe subscription tiers, Livewire classic component structure, and Pest testing standards.
This commit is contained in:
Ovidiu U
2026-04-03 16:49:19 +01:00
parent c94c4f7beb
commit 02d4c9d888
11 changed files with 644 additions and 0 deletions

76
scoring.md Normal file
View File

@@ -0,0 +1,76 @@
# Scoring Engine (AlertScoringService)
## Purpose
Produces a "fill up now or wait?" recommendation per user based on their local
station history. Output is one of: `fill_up`, `wait`, `no_signal`.
Never guess — stay silent (no_signal) when signals conflict or data is insufficient.
## The 4 signals (in priority order)
### Signal 1 — Local price trend (HIGHEST WEIGHT)
- Query `station_prices` for user's nearest 5 stations (within 5km of user lat/lng)
- Use last 14 days of history for `e10` (or user's preferred fuel type)
- Calculate 3-day rolling average vs 7-day rolling average
- **Falling**: 3-day avg < 7-day avg by 0.5p positive wait signal
- **Rising**: 3-day avg > 7-day avg by ≥ 0.5p → fill_up signal
- **Flat**: difference < 0.5p neutral, no signal
- Weight: 40 points max
### Signal 2 — Supermarket anchor effect (HIGH WEIGHT)
- Find nearest supermarket station (is_supermarket = 1) within 10km
- Check if supermarket cut price in last 48 hours (> 1p drop)
- Check if nearest non-supermarket stations have NOT yet followed
- If supermarket cut AND independents haven't moved → strong wait signal
- Weight: 35 points max
### Signal 3 — Day-of-week pattern (MEDIUM WEIGHT — needs 8+ weeks data)
- Per station: average price by day-of-week over last 90 days
- Only activate if station has 56+ days of history
- If today is statistically 1.5p+ cheaper than weekly average → mild fill_up
- If today is statistically 1.5p+ more expensive → mild wait
- Weight: 15 points max
### Signal 4 — Brent crude direction (LOW WEIGHT)
- Fetched daily from FRED API, stored in a simple `brent_prices` table
- 5-day trend: rising ≥ 3% → mild fill_up pressure; falling ≥ 3% → mild wait
- Weight: 10 points max
## Confidence thresholds
- Score 70100: strong signal → fire recommendation + notification
- Score 4069: weak signal → show in dashboard only, no push/SMS/WhatsApp
- Score 039: no_signal → stay silent entirely
**Only send notifications when confidence ≥ 70.** Never spam.
## Output (stored in scoring_results)
```php
[
'recommendation' => 'wait', // fill_up | wait | no_signal
'confidence' => 78, // 0-100
'signals' => [
'trend' => ['direction' => 'falling', 'points' => 32],
'supermarket' => ['triggered' => true, 'points' => 35],
'day_pattern' => ['triggered' => false, 'points' => 0],
'brent' => ['direction' => 'flat', 'points' => 0],
],
'local_avg_pence' => 14380, // 143.80p
'trend_delta' => -2.3, // pence change over 7 days
]
```
## Human-readable reason strings
Always generate a plain-English reason for the recommendation:
- "Prices near you have been falling for 6 days. Tesco {station} cut 3p yesterday — independents usually follow within 48 hours."
- "Prices are rising in your area — filling up today avoids paying more later."
- "No clear pattern this week — fill up at the cheapest station near you now."
Reason strings are stored in `scoring_results.signals` JSON and shown in the UI and notifications.
## Accuracy self-tracking
After 3 days, check if `wait` recommendation was correct (prices did fall further).
Store outcome in `scoring_results` for future display: "This signal has been right X% of the time in your area."