|
| 1 | +# Data Model: Gas Price Lookup at Ride Entry |
| 2 | + |
| 3 | +**Feature**: 010-gas-price-lookup |
| 4 | +**Branch**: `010-gas-price-lookup` |
| 5 | +**Date**: 2026-03-31 |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## New Entity: GasPriceLookup |
| 10 | + |
| 11 | +The durable cache for EIA gas price responses. One row per calendar date. Immutable after creation. |
| 12 | + |
| 13 | +| Column | Type | Constraints | Notes | |
| 14 | +|---|---|---|---| |
| 15 | +| `GasPriceLookupId` | `INTEGER` PK | NOT NULL, AUTOINCREMENT | Surrogate key | |
| 16 | +| `PriceDate` | `TEXT` (date) | NOT NULL, UNIQUE | Calendar date the price applies to (YYYY-MM-DD). Unique index — one price per date. | |
| 17 | +| `PricePerGallon` | `DECIMAL(10,4)` | NOT NULL | National average retail price in USD per gallon; 4 decimal places. | |
| 18 | +| `DataSource` | `TEXT(64)` | NOT NULL | Identifier for the source (e.g., `"EIA_EPM0_NUS_Weekly"`) | |
| 19 | +| `EiaPeriodDate` | `TEXT` (date) | NOT NULL | The actual EIA `period` date returned (the Monday of the surveyed week). May differ from `PriceDate` when the lookup uses the nearest prior week. | |
| 20 | +| `RetrievedAtUtc` | `TEXT` (datetime) | NOT NULL | When the cache entry was written. | |
| 21 | + |
| 22 | +**Indexes**: |
| 23 | +- `UNIQUE (PriceDate)` — enforced at DB level to prevent duplicate cache entries for the same date. |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Modified Entity: Ride |
| 28 | + |
| 29 | +New column added to `Rides` table. |
| 30 | + |
| 31 | +| Column | Type | Constraints | Notes | |
| 32 | +|---|---|---|---| |
| 33 | +| `GasPricePerGallon` | `DECIMAL(10,4)` | NULLABLE | The gas price per gallon in effect at the time of the ride date. Null if unavailable at time of creation/edit. | |
| 34 | + |
| 35 | +--- |
| 36 | + |
| 37 | +## Modified Domain Events |
| 38 | + |
| 39 | +### RideRecordedEventPayload (C# record) |
| 40 | + |
| 41 | +Add field: |
| 42 | + |
| 43 | +| Field | Type | Notes | |
| 44 | +|---|---|---| |
| 45 | +| `GasPricePerGallon` | `decimal?` | Optional. The gas price stored with this ride creation event. | |
| 46 | + |
| 47 | +### RideEditedEventPayload (C# record) |
| 48 | + |
| 49 | +Add field: |
| 50 | + |
| 51 | +| Field | Type | Notes | |
| 52 | +|---|---|---| |
| 53 | +| `GasPricePerGallon` | `decimal?` | Optional. The gas price stored with this ride edit event. Reflects the price for the (possibly changed) ride date. | |
| 54 | + |
| 55 | +--- |
| 56 | + |
| 57 | +## Modified API Contracts (C#) |
| 58 | + |
| 59 | +### RecordRideRequest |
| 60 | + |
| 61 | +Add field: |
| 62 | + |
| 63 | +| Field | Type | Validation | Notes | |
| 64 | +|---|---|---|---| |
| 65 | +| `GasPricePerGallon` | `decimal?` | `[Range(0.01, 999.9999)]` optional | User-submitted gas price from the form field. Null if the user left the field empty. | |
| 66 | + |
| 67 | +### EditRideRequest |
| 68 | + |
| 69 | +Add field: |
| 70 | + |
| 71 | +| Field | Type | Validation | Notes | |
| 72 | +|---|---|---|---| |
| 73 | +| `GasPricePerGallon` | `decimal?` | `[Range(0.01, 999.9999)]` optional | User-submitted gas price from the form field. Null if left empty. | |
| 74 | + |
| 75 | +### RideDefaultsResponse |
| 76 | + |
| 77 | +Add field: |
| 78 | + |
| 79 | +| Field | Type | Notes | |
| 80 | +|---|---|---| |
| 81 | +| `DefaultGasPricePerGallon` | `decimal?` | The gas price from the most recent saved ride for this user. Null if no prior rides or no prior price. | |
| 82 | + |
| 83 | +### New: GasPriceResponse |
| 84 | + |
| 85 | +Returned by `GET /api/rides/gas-price?date=YYYY-MM-DD`. |
| 86 | + |
| 87 | +| Field | Type | Notes | |
| 88 | +|---|---|---| |
| 89 | +| `Date` | `string` (YYYY-MM-DD) | The requested date. | |
| 90 | +| `PricePerGallon` | `decimal?` | The retrieved or cached price. Null if unavailable. | |
| 91 | +| `IsAvailable` | `bool` | `true` when a price was found; `false` otherwise. | |
| 92 | +| `DataSource` | `string?` | Identifier for the source (e.g., `"EIA_EPM0_NUS_Weekly"`). Null when unavailable. | |
| 93 | + |
| 94 | +--- |
| 95 | + |
| 96 | +## Modified Frontend TypeScript Interfaces |
| 97 | + |
| 98 | +### RecordRideRequest (TypeScript) |
| 99 | +```typescript |
| 100 | +gasPricePerGallon?: number; |
| 101 | +``` |
| 102 | + |
| 103 | +### EditRideRequest (TypeScript) |
| 104 | +```typescript |
| 105 | +gasPricePerGallon?: number; |
| 106 | +``` |
| 107 | + |
| 108 | +### RideDefaultsResponse (TypeScript) |
| 109 | +```typescript |
| 110 | +defaultGasPricePerGallon?: number; |
| 111 | +``` |
| 112 | + |
| 113 | +### RideHistoryRow (TypeScript) |
| 114 | +```typescript |
| 115 | +gasPricePerGallon?: number; |
| 116 | +``` |
| 117 | + |
| 118 | +### New: GasPriceResponse (TypeScript) |
| 119 | +```typescript |
| 120 | +interface GasPriceResponse { |
| 121 | + date: string; |
| 122 | + pricePerGallon: number | null; |
| 123 | + isAvailable: boolean; |
| 124 | + dataSource: string | null; |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +--- |
| 129 | + |
| 130 | +## State Transitions |
| 131 | + |
| 132 | +``` |
| 133 | +Ride form loads |
| 134 | + ├─ Call GET /api/rides/defaults |
| 135 | + │ └─ Returns DefaultGasPricePerGallon from last ride (or null) |
| 136 | + │ └─ Pre-populate gas price field |
| 137 | + │ |
| 138 | + ├─ User changes ride date (debounced 300ms) |
| 139 | + │ └─ Call GET /api/rides/gas-price?date=NEW_DATE |
| 140 | + │ ├─ Cache HIT → return cached price → update field |
| 141 | + │ ├─ Cache MISS → fetch EIA API → store in cache → return price → update field |
| 142 | + │ └─ EIA unavailable / no data → return isAvailable=false → field unchanged (retains default/prior value) |
| 143 | + │ |
| 144 | + └─ User submits form |
| 145 | + └─ gasPricePerGallon = current field value (or null) |
| 146 | + └─ Stored in RideRecordedEvent / RideEditedEvent + RideEntity |
| 147 | +``` |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +## Database Migration |
| 152 | + |
| 153 | +**Migration name pattern**: `YYYYMMDDHHMMSS_AddGasPriceToRidesAndLookupCache` |
| 154 | + |
| 155 | +Changes: |
| 156 | +1. Create `GasPriceLookups` table with columns above. |
| 157 | +2. Add `GasPricePerGallon DECIMAL(10,4) NULL` column to `Rides` table. |
| 158 | +3. Create unique index on `GasPriceLookups(PriceDate)`. |
0 commit comments