feat(CRIT-010): add oil_prices API endpoint with zone-to-county mapping

Add GET /oil-prices/county/:county_id public endpoint to serve scraped
oil price data by county. Includes oil_prices table definition in schema
and a backfill script to populate county_id on existing records.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 18:31:38 -05:00
parent caa318508b
commit 85bbe43192
6 changed files with 134 additions and 1 deletions

63
db/backfill_county_id.sql Normal file
View File

@@ -0,0 +1,63 @@
-- Backfill county_id for existing oil_prices records based on state+zone mapping
-- Run this ONCE after adding the county_id column to oil_prices
-- First, add the column if it doesn't exist
ALTER TABLE oil_prices ADD COLUMN IF NOT EXISTS county_id INTEGER;
-- Connecticut
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'New London' AND state = 'CT') WHERE LOWER(state) = 'connecticut' AND zone = 1 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Windham' AND state = 'CT') WHERE LOWER(state) = 'connecticut' AND zone = 2 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'New Haven' AND state = 'CT') WHERE LOWER(state) = 'connecticut' AND zone = 3 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Middlesex' AND state = 'CT') WHERE LOWER(state) = 'connecticut' AND zone = 4 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'New Haven' AND state = 'CT') WHERE LOWER(state) = 'connecticut' AND zone = 5 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Hartford' AND state = 'CT') WHERE LOWER(state) = 'connecticut' AND zone = 6 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Litchfield' AND state = 'CT') WHERE LOWER(state) = 'connecticut' AND zone = 7 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Fairfield' AND state = 'CT') WHERE LOWER(state) = 'connecticut' AND zone = 8 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Tolland' AND state = 'CT') WHERE LOWER(state) = 'connecticut' AND zone = 9 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Litchfield' AND state = 'CT') WHERE LOWER(state) = 'connecticut' AND zone = 10 AND county_id IS NULL;
-- Massachusetts
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Suffolk' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 1 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Middlesex' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 2 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Norfolk' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 3 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Plymouth' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 4 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Middlesex' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 5 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Bristol' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 6 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Barnstable' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 7 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Essex' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 8 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Essex' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 9 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Worcester' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 10 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Worcester' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 11 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Hampshire' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 12 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Hampden' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 13 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Franklin' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 14 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Berkshire' AND state = 'MA') WHERE LOWER(state) = 'massachusetts' AND zone = 15 AND county_id IS NULL;
-- New Hampshire
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Coos' AND state = 'NH') WHERE LOWER(state) = 'newhampshire' AND zone = 1 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Strafford' AND state = 'NH') WHERE LOWER(state) = 'newhampshire' AND zone = 2 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Merrimack' AND state = 'NH') WHERE LOWER(state) = 'newhampshire' AND zone = 3 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Grafton' AND state = 'NH') WHERE LOWER(state) = 'newhampshire' AND zone = 4 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Cheshire' AND state = 'NH') WHERE LOWER(state) = 'newhampshire' AND zone = 5 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Hillsborough' AND state = 'NH') WHERE LOWER(state) = 'newhampshire' AND zone = 6 AND county_id IS NULL;
-- Rhode Island
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Newport' AND state = 'RI') WHERE LOWER(state) = 'rhodeisland' AND zone = 1 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Providence' AND state = 'RI') WHERE LOWER(state) = 'rhodeisland' AND zone = 2 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Washington' AND state = 'RI') WHERE LOWER(state) = 'rhodeisland' AND zone = 3 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Kent' AND state = 'RI') WHERE LOWER(state) = 'rhodeisland' AND zone = 4 AND county_id IS NULL;
-- Maine
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Cumberland' AND state = 'ME') WHERE LOWER(state) = 'maine' AND zone = 1 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Kennebec' AND state = 'ME') WHERE LOWER(state) = 'maine' AND zone = 2 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Androscoggin' AND state = 'ME') WHERE LOWER(state) = 'maine' AND zone = 3 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'York' AND state = 'ME') WHERE LOWER(state) = 'maine' AND zone = 4 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Knox' AND state = 'ME') WHERE LOWER(state) = 'maine' AND zone = 5 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Penobscot' AND state = 'ME') WHERE LOWER(state) = 'maine' AND zone = 6 AND county_id IS NULL;
UPDATE oil_prices SET county_id = (SELECT id FROM county WHERE name = 'Washington' AND state = 'ME') WHERE LOWER(state) = 'maine' AND zone = 7 AND county_id IS NULL;
-- Verify
SELECT state, zone, county_id, COUNT(*) as record_count
FROM oil_prices
GROUP BY state, zone, county_id
ORDER BY state, zone;

View File

@@ -58,6 +58,22 @@ CREATE TABLE listings (
last_edited TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Oil prices (scraped from NewEnglandOil.com / MaineOil.com)
CREATE TABLE oil_prices (
id SERIAL PRIMARY KEY,
state VARCHAR(100),
zone INTEGER,
name VARCHAR(255),
price DOUBLE PRECISION,
date VARCHAR(20),
scrapetimestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
company_id INTEGER,
county_id INTEGER
);
-- If the table already exists, add county_id
-- ALTER TABLE oil_prices ADD COLUMN county_id INTEGER;
-- If the table already exists, add the new columns
-- ALTER TABLE listings ADD COLUMN price_per_gallon_cash DOUBLE PRECISION;
-- ALTER TABLE listings ADD COLUMN note TEXT;

View File

@@ -9,6 +9,7 @@ use crate::auth::auth::{auth_middleware, login, register, logout};
use crate::data::data::get_user;
use crate::state::data::{get_counties_by_state, get_county_by_id};
use crate::listing::data::{get_listings, get_listing_by_id, get_listings_by_county, create_listing, update_listing, delete_listing};
use crate::oil_prices::data::get_oil_prices_by_county;
use axum::middleware;
use sqlx::PgPool;
use std::sync::Arc;
@@ -18,6 +19,7 @@ mod data;
mod state;
mod company;
mod listing;
mod oil_prices;
#[tokio::main]
async fn main() {
@@ -82,7 +84,8 @@ async fn main() {
.route("/categories", axum::routing::get(crate::company::category::get_all_categories))
.route("/state/:state_abbr", axum::routing::get(get_counties_by_state))
.route("/state/:state_abbr/:county_id", axum::routing::get(get_county_by_id))
.route("/listings/county/:county_id", axum::routing::get(get_listings_by_county));
.route("/listings/county/:county_id", axum::routing::get(get_listings_by_county))
.route("/oil-prices/county/:county_id", axum::routing::get(get_oil_prices_by_county));
let app = public_routes
.merge(protected_routes)

34
src/oil_prices/data.rs Normal file
View File

@@ -0,0 +1,34 @@
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use crate::auth::structs::AppState;
use crate::oil_prices::structs::OilPrice;
use serde_json::json;
pub async fn get_oil_prices_by_county(
State(app_state): State<AppState>,
Path(county_id): Path<i32>,
) -> Result<Json<Vec<OilPrice>>, (StatusCode, Json<serde_json::Value>)> {
tracing::info!(county_id = county_id, "Fetching oil prices by county");
match sqlx::query_as::<_, OilPrice>(
"SELECT id, state, zone, name, price, date, scrapetimestamp, county_id FROM oil_prices WHERE county_id = $1 ORDER BY price ASC"
)
.bind(county_id)
.fetch_all(&*app_state.db)
.await
{
Ok(prices) => {
tracing::info!(county_id = county_id, count = prices.len(), "Oil prices retrieved");
Ok(Json(prices))
},
Err(e) => {
tracing::error!(county_id = county_id, error = %e, "Failed to fetch oil prices");
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": format!("Failed to fetch oil prices: {}", e)}))
))
},
}
}

2
src/oil_prices/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod structs;
pub mod data;

15
src/oil_prices/structs.rs Normal file
View File

@@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use chrono::NaiveDateTime;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct OilPrice {
pub id: i32,
pub state: Option<String>,
pub zone: Option<i32>,
pub name: Option<String>,
pub price: Option<f64>,
pub date: Option<String>,
pub scrapetimestamp: Option<NaiveDateTime>,
pub county_id: Option<i32>,
}