Skip to content

Commit 77378f0

Browse files
author
aligneddev
committed
plan
1 parent e3b0326 commit 77378f0

9 files changed

Lines changed: 949 additions & 1 deletion

File tree

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"/^cd /workspaces/neCodeBikeTracking && pwsh \\.specify/scripts/powershell/check-prerequisites\\.ps1 -Json$/": {
1313
"approve": true,
1414
"matchCommandLine": true
15-
}
15+
},
16+
"mkdir": true
1617
},
1718
"sqltools.connections": [
1819
{
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Specification Quality Checklist: Gas Price Lookup at Ride Entry
2+
3+
**Purpose**: Validate specification completeness and quality before proceeding to planning
4+
**Created**: 2026-03-31
5+
**Feature**: [spec.md](../spec.md)
6+
7+
## Content Quality
8+
9+
- [x] No implementation details (languages, frameworks, APIs)
10+
- [x] Focused on user value and business needs
11+
- [x] Written for non-technical stakeholders
12+
- [x] All mandatory sections completed
13+
14+
## Requirement Completeness
15+
16+
- [x] No [NEEDS CLARIFICATION] markers remain — FR-012 resolved: EIA API with team-managed key; secret storage deferred to a separate concern
17+
- [x] Requirements are testable and unambiguous
18+
- [x] Success criteria are measurable
19+
- [x] Success criteria are technology-agnostic (no implementation details)
20+
- [x] All acceptance scenarios are defined
21+
- [x] Edge cases are identified
22+
- [x] Scope is clearly bounded
23+
- [x] Dependencies and assumptions identified
24+
25+
## Feature Readiness
26+
27+
- [x] All functional requirements have clear acceptance criteria
28+
- [x] User scenarios cover primary flows
29+
- [x] Feature meets measurable outcomes defined in Success Criteria
30+
- [x] No implementation details leak into specification
31+
32+
## Notes
33+
34+
- FR-010 resolved: EIA API (U.S. government source) with a free team-managed API key. Secret storage mechanism (e.g., KeyVault, environment variable) is deferred and out of scope for this feature.
35+
- All items pass. The spec is ready for `/speckit.plan`.
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# API Contract: Gas Price Lookup Endpoint
2+
3+
**Feature**: 010-gas-price-lookup
4+
**Owner**: BikeTracking.Api
5+
**Consumer**: BikeTracking.Frontend
6+
7+
---
8+
9+
## GET /api/rides/gas-price
10+
11+
Retrieves the national average retail gasoline price for a given date, using a local durable cache backed by the EIA API.
12+
13+
### Request
14+
15+
```
16+
GET /api/rides/gas-price?date=YYYY-MM-DD
17+
Authorization: Bearer {token}
18+
```
19+
20+
**Query Parameters**
21+
22+
| Parameter | Type | Required | Constraints | Notes |
23+
|---|---|---|---|---|
24+
| `date` | string (YYYY-MM-DD) | Yes | Valid ISO date format | The ride date to look up the gas price for. |
25+
26+
### Response: 200 OK
27+
28+
```json
29+
{
30+
"date": "2026-03-31",
31+
"pricePerGallon": 3.1860,
32+
"isAvailable": true,
33+
"dataSource": "EIA_EPM0_NUS_Weekly"
34+
}
35+
```
36+
37+
**When unavailable** (API down, no data, future date with no coverage):
38+
```json
39+
{
40+
"date": "2100-01-01",
41+
"pricePerGallon": null,
42+
"isAvailable": false,
43+
"dataSource": null
44+
}
45+
```
46+
47+
### Response: 400 Bad Request
48+
49+
Returned when `date` is missing or not a valid date string.
50+
51+
```json
52+
{
53+
"error": "invalid_request",
54+
"message": "date query parameter is required and must be a valid date in YYYY-MM-DD format."
55+
}
56+
```
57+
58+
### Response: 401 Unauthorized
59+
60+
Returned when no valid bearer token is present.
61+
62+
### Notes
63+
64+
- This endpoint never returns a 5xx for EIA lookup failures. EIA failures are absorbed and reflected as `isAvailable: false` with `pricePerGallon: null`.
65+
- The response is deterministic for any given date: once a price is cached, the same value is always returned for that date.
66+
- The `date` parameter is used as the cache key. The actual EIA period date (the Monday of the survey week) may differ; it is not exposed in this contract.
67+
68+
---
69+
70+
## Modified Contract: GET /api/rides/defaults
71+
72+
Extends the existing defaults endpoint to include the most recent ride's gas price.
73+
74+
### Response: 200 OK (extended)
75+
76+
Adds `defaultGasPricePerGallon` to the existing response:
77+
78+
```json
79+
{
80+
"hasPreviousRide": true,
81+
"defaultRideDateTimeLocal": "2026-03-31T07:30:00",
82+
"defaultMiles": 5.2,
83+
"defaultRideMinutes": 22,
84+
"defaultTemperature": 58.0,
85+
"defaultGasPricePerGallon": 3.1860
86+
}
87+
```
88+
89+
When no previous ride exists, or the most recent ride has no gas price:
90+
```json
91+
{
92+
"hasPreviousRide": false,
93+
"defaultRideDateTimeLocal": "2026-03-31T08:00:00",
94+
"defaultGasPricePerGallon": null
95+
}
96+
```
97+
98+
**Backwards compatibility**: `defaultGasPricePerGallon` is a new nullable field. Existing clients that ignore it continue to work.
99+
100+
---
101+
102+
## Modified Contract: POST /api/rides (Record Ride)
103+
104+
Adds `gasPricePerGallon` to the existing request body.
105+
106+
### Request (extended)
107+
108+
```json
109+
{
110+
"rideDateTimeLocal": "2026-03-31T07:30:00",
111+
"miles": 5.2,
112+
"rideMinutes": 22,
113+
"temperature": 58.0,
114+
"gasPricePerGallon": 3.1860
115+
}
116+
```
117+
118+
| Field | Type | Required | Constraints |
119+
|---|---|---|---|
120+
| `gasPricePerGallon` | number | No | Must be > 0 and ≤ 999.9999 when provided. Null/omitted means unavailable. |
121+
122+
**Backwards compatibility**: Existing requests that omit `gasPricePerGallon` continue to work; the field defaults to null.
123+
124+
---
125+
126+
## Modified Contract: PUT /api/rides/{rideId} (Edit Ride)
127+
128+
Adds `gasPricePerGallon` to the existing request body.
129+
130+
### Request (extended)
131+
132+
```json
133+
{
134+
"rideDateTimeLocal": "2026-03-31T07:30:00",
135+
"miles": 5.2,
136+
"rideMinutes": 22,
137+
"temperature": 58.0,
138+
"gasPricePerGallon": 3.1860,
139+
"expectedVersion": 2
140+
}
141+
```
142+
143+
| Field | Type | Required | Constraints |
144+
|---|---|---|---|
145+
| `gasPricePerGallon` | number | No | Must be > 0 and ≤ 999.9999 when provided. Null/omitted means price not available. |
146+
147+
**Backwards compatibility**: Existing clients that omit `gasPricePerGallon` continue to work.
148+
149+
---
150+
151+
## Modified Contract: GET /api/rides/history (Ride History Row)
152+
153+
Adds `gasPricePerGallon` to each ride row in the history response.
154+
155+
### RideHistoryRow (extended)
156+
157+
```json
158+
{
159+
"rideId": 42,
160+
"rideDateTimeLocal": "2026-03-31T07:30:00",
161+
"miles": 5.2,
162+
"rideMinutes": 22,
163+
"temperature": 58.0,
164+
"gasPricePerGallon": 3.1860
165+
}
166+
```
167+
168+
**Backwards compatibility**: New nullable field; existing consumers that ignore it continue to work.
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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

Comments
 (0)