A whole lot of stuff

This commit is contained in:
Courtney Arnold 2026-08-18 11:04:58 -05:00
parent faf319322a
commit ad850d2be6
50 changed files with 3013 additions and 213 deletions

73
docs/erd.md Normal file
View file

@ -0,0 +1,73 @@
# SQLite entity-relationship diagram
Local database: `ApplicationDocumentsDirectory/FuelTaxTracker/fuel_tax_tracker.db`
Schema version: **5** (defined in `lib/services/database_service.dart` / `lib/services/db_schema.dart`)
```mermaid
erDiagram
VEHICLES ||--o{ FUEL_ENTRIES : "owns (vehicle_id)"
VEHICLES {
TEXT id PK "UUID, hidden, immutable"
TEXT vin "required; unique among active rows, app-enforced"
TEXT nickname "nullable display label"
INTEGER updated_at "UTC millis; newest-wins merge key"
INTEGER deleted_at "nullable soft-delete tombstone"
INTEGER dirty "local-only; 1 = not yet pushed"
}
FUEL_ENTRIES {
TEXT id PK "UUID"
TEXT vehicle_id FK "references vehicles.id — no SQLite FK"
INTEGER date "purchase datetime, local millis"
REAL gallons
REAL price_per_gallon
REAL total_cost
TEXT receipt_image_path "nullable on-device path"
TEXT receipt_drive_file_id "nullable cloud file id"
INTEGER updated_at "UTC millis; newest-wins merge key"
INTEGER deleted_at "nullable soft-delete tombstone"
INTEGER dirty "local-only; 1 = not yet pushed"
}
AD_FREE_ENTITLEMENT {
INTEGER id PK "always 1 (CHECK id = 1)"
INTEGER ad_free_until "nullable UTC millis"
INTEGER updated_at "UTC millis; newest-wins merge key"
}
```
## Relationships
| From | To | Cardinality | Enforced by |
| --- | --- | --- | --- |
| `vehicles` | `fuel_entries` | 1 : 0..n | Application (`FuelEntry.vehicleId` → `Vehicle.id`). There is **no** `FOREIGN KEY` clause. |
| `ad_free_entitlement` | — | singleton | `PRIMARY KEY CHECK (id = 1)` |
VIN is **not** the foreign key. Fuel entries reference the hidden vehicle `id` so the user can edit a VIN without breaking receipts or sync history.
## Indexes and constraints
- `vehicles.id` — `PRIMARY KEY`
- `fuel_entries.id` — `PRIMARY KEY`
- `idx_fuel_entries_vehicle_id` on `fuel_entries(vehicle_id)`
- `ad_free_entitlement.id` — `PRIMARY KEY CHECK (id = 1)`
- VIN uniqueness among **active** (`deleted_at IS NULL`) vehicles is enforced in `DatabaseService.vinExists` / `AppState.addVehicle` / `AppState.updateVehicle`, not by a unique index. Two offline devices can independently add the same VIN; that rare conflict is accepted rather than merged field-by-field.
## Sync and lifecycle columns
Every synced table carries `updated_at`. Cloud merge is last-write-wins on that timestamp (`INSERT OR REPLACE` of a remote row that is new or newer). See `mergeVehiclesSql`, `mergeFuelEntriesSql`, and `mergeAdFreeEntitlementSql` in `db_schema.dart`.
| Column | Scope | Meaning |
| --- | --- | --- |
| `updated_at` | all three tables | Newest value wins when merging a remote copy. |
| `deleted_at` | `vehicles`, `fuel_entries` | Soft-delete tombstone. Rows are never hard-deleted, so a deletion can propagate to other devices instead of being resurrected by a stale remote copy. |
| `dirty` | `vehicles`, `fuel_entries` only | Local bookkeeping: `1` = not yet pushed. Cleared to `0` on merge-in. **Not** mapped onto the Dart models. |
| `receipt_image_path` | `fuel_entries` | Device-local filesystem path. Forced to `NULL` when merging a remote row — a path from another phone is never valid here. |
| `receipt_drive_file_id` | `fuel_entries` | Opaque id of the uploaded receipt on the active cloud provider. Presence means “already uploaded”; a pending upload is `path IS NOT NULL AND drive_file_id IS NULL`. |
`ad_free_entitlement` has no `dirty` or `deleted_at`. It is a single row that exists so a consumable Play Store purchase (which cannot be restored after consume) can survive reinstall via the same cloud merge as vehicles and fuel entries.
## What is *not* in SQLite
Preferences live in `SharedPreferences`, not this database: theme, onboarding/agreement flags, cloud provider + folder ids, keep-photos-locally, stale-lock timeout, estimated-refund toggle.