Files
fuel-price/.claude/rules/database.md
Ovidiu U 5ad89e977d Add fuel API ingestion and historic storage design spec
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>
2026-04-03 18:09:50 +01:00

99 lines
3.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 `$fillable` or `$guarded` on models — never leave both empty.
- Use `->comment()` on migration columns for non-obvious fields.
## Core tables
### users
Standard Laravel users table + additions:
- `postcode` VARCHAR(8) — user's home postcode, used for nearby station lookup
- `lat` / `lng` DECIMAL(10,7) — resolved from postcode via postcodes.io on registration
- `whatsapp_number` VARCHAR(20) NULLABLE — verified mobile, set after OTP flow
- `whatsapp_verified_at` TIMESTAMP NULLABLE
- `push_token` VARCHAR(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 — 0100
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.