Includes verified API authentication flow, correct base URL, all DB table schemas for stations, current prices, history, and archive. Fuel types corrected to match live API (B7_STANDARD, B7_PREMIUM). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3.5 KiB
3.5 KiB
Database
Engine and conventions
- MySQL with InnoDB. All schema changes via Laravel migrations only — no raw DDL.
- All models use Eloquent. No raw DB:: queries except for complex aggregations.
- Table names: plural snake_case. Column names: snake_case.
- Always define
$fillableor$guardedon models — never leave both empty. - Use
->comment()on migration columns for non-obvious fields.
Core tables
users
Standard Laravel users table + additions:
postcodeVARCHAR(8) — user's home postcode, used for nearby station lookuplat/lngDECIMAL(10,7) — resolved from postcode via postcodes.io on registrationwhatsapp_numberVARCHAR(20) NULLABLE — verified mobile, set after OTP flowwhatsapp_verified_atTIMESTAMP NULLABLEpush_tokenVARCHAR(255) NULLABLE — OneSignal player ID
subscriptions
Managed by Laravel Cashier — do not hand-edit this table.
Tier is read via $user->subscribed('basic'), ->subscribed('plus'), ->subscribed('pro').
station_prices (HIGH VOLUME — partitioned)
id BIGINT UNSIGNED AUTO_INCREMENT
station_id VARCHAR(64) — Fuel Finder API station identifier
fuel_type ENUM('e10','e5','b7_standard','b7_premium','b10','hvo')
price_pence SMALLINT UNSIGNED — e.g. 14523 = 145.23p (store as integer × 100)
is_supermarket TINYINT(1) DEFAULT 0
brand VARCHAR(64) NULLABLE
recorded_at DATETIME
INDEX (station_id, fuel_type, recorded_at)
INDEX (recorded_at)
PARTITION BY RANGE (YEAR(recorded_at) * 100 + MONTH(recorded_at))
Partition monthly. Use recorded_at not created_at — it reflects actual price time.
Never store floats for prices — always pence as SMALLINT (price × 100).
stations (reference/cache)
station_id VARCHAR(64) PRIMARY KEY — from Fuel Finder API
name VARCHAR(128)
brand VARCHAR(64) NULLABLE
lat DECIMAL(10,7)
lng DECIMAL(10,7)
postcode VARCHAR(8)
is_supermarket TINYINT(1) DEFAULT 0
amenities JSON NULLABLE
last_seen_at DATETIME
Refreshed on each Fuel Finder poll. is_supermarket set by StationTaggingService.
alerts (sent notification log)
id BIGINT UNSIGNED AUTO_INCREMENT
user_id BIGINT UNSIGNED FK users.id
alert_type ENUM('fill_up_now','wait','watchdog','price_drop')
channel ENUM('email','whatsapp','sms','push')
signal_strength TINYINT — 1=weak, 2=medium, 3=strong
payload JSON — recommendation data snapshot
sent_at DATETIME
INDEX (user_id, sent_at)
scoring_results (daily per-user cache)
user_id BIGINT UNSIGNED FK users.id
scored_at DATE
recommendation ENUM('fill_up','wait','no_signal')
confidence TINYINT — 0–100
signals JSON — breakdown of each signal used
local_avg_pence SMALLINT UNSIGNED
trend_direction ENUM('falling','rising','flat')
expires_at DATETIME
PRIMARY KEY (user_id, scored_at)
otp_verifications
id BIGINT UNSIGNED AUTO_INCREMENT
user_id BIGINT UNSIGNED FK users.id
channel ENUM('whatsapp','sms')
phone_number VARCHAR(20)
code CHAR(6)
expires_at DATETIME
used_at DATETIME NULLABLE
INDEX (user_id, expires_at)
OTP codes expire after 10 minutes. Mark used_at on success — never delete rows.
Supermarket brands (StationTaggingService)
Match station name (case-insensitive) against:
['tesco', 'asda', 'morrisons', 'sainsbury', 'aldi', 'lidl', 'costco']
Set is_supermarket = 1 and normalise brand on match.