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:
63
db/backfill_county_id.sql
Normal file
63
db/backfill_county_id.sql
Normal 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;
|
||||||
16
schema.sql
16
schema.sql
@@ -58,6 +58,22 @@ CREATE TABLE listings (
|
|||||||
last_edited TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
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
|
-- 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 price_per_gallon_cash DOUBLE PRECISION;
|
||||||
-- ALTER TABLE listings ADD COLUMN note TEXT;
|
-- ALTER TABLE listings ADD COLUMN note TEXT;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::auth::auth::{auth_middleware, login, register, logout};
|
|||||||
use crate::data::data::get_user;
|
use crate::data::data::get_user;
|
||||||
use crate::state::data::{get_counties_by_state, get_county_by_id};
|
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::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 axum::middleware;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -18,6 +19,7 @@ mod data;
|
|||||||
mod state;
|
mod state;
|
||||||
mod company;
|
mod company;
|
||||||
mod listing;
|
mod listing;
|
||||||
|
mod oil_prices;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -82,7 +84,8 @@ async fn main() {
|
|||||||
.route("/categories", axum::routing::get(crate::company::category::get_all_categories))
|
.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", axum::routing::get(get_counties_by_state))
|
||||||
.route("/state/:state_abbr/:county_id", axum::routing::get(get_county_by_id))
|
.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
|
let app = public_routes
|
||||||
.merge(protected_routes)
|
.merge(protected_routes)
|
||||||
|
|||||||
34
src/oil_prices/data.rs
Normal file
34
src/oil_prices/data.rs
Normal 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
2
src/oil_prices/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod structs;
|
||||||
|
pub mod data;
|
||||||
15
src/oil_prices/structs.rs
Normal file
15
src/oil_prices/structs.rs
Normal 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>,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user