feat: add neighborhood asset and scoring api
This commit is contained in:
@@ -4,8 +4,9 @@ use serde::Serialize;
|
||||
|
||||
use crate::error::{ApiError, ApiResult};
|
||||
use crate::models::{
|
||||
AreaScore, AreaScoreSummary, CreateWatchlistItem, MarketOverview, MonthQuery,
|
||||
UpdateWatchlistItem, WatchlistItem,
|
||||
AreaScore, AreaScoreSummary, CreateWatchlistItem, MarketOverview, MonthQuery, Neighborhood,
|
||||
NeighborhoodQuery, NeighborhoodScore, NeighborhoodScoreQuery, UpdateWatchlistItem,
|
||||
WatchlistItem,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
|
||||
@@ -70,6 +71,111 @@ pub async fn market_overview(
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn list_neighborhoods(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<NeighborhoodQuery>,
|
||||
) -> ApiResult<Json<Vec<Neighborhood>>> {
|
||||
let neighborhoods = sqlx::query_as::<_, Neighborhood>(
|
||||
r#"
|
||||
SELECT
|
||||
n.neighborhood_id,
|
||||
n.area_id,
|
||||
a.name AS area_name,
|
||||
a.district,
|
||||
n.name,
|
||||
n.built_year,
|
||||
n.property_type,
|
||||
n.metro_distance_m,
|
||||
n.school_quality,
|
||||
n.notes
|
||||
FROM silver.neighborhoods n
|
||||
JOIN silver.areas a ON a.area_id = n.area_id
|
||||
WHERE ($1::text IS NULL OR n.area_id = $1)
|
||||
ORDER BY a.district, a.name, n.name
|
||||
"#,
|
||||
)
|
||||
.bind(query.area_id)
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
Ok(Json(neighborhoods))
|
||||
}
|
||||
|
||||
pub async fn get_neighborhood(
|
||||
State(state): State<AppState>,
|
||||
Path(neighborhood_id): Path<String>,
|
||||
) -> ApiResult<Json<Neighborhood>> {
|
||||
let neighborhood = sqlx::query_as::<_, Neighborhood>(
|
||||
r#"
|
||||
SELECT
|
||||
n.neighborhood_id,
|
||||
n.area_id,
|
||||
a.name AS area_name,
|
||||
a.district,
|
||||
n.name,
|
||||
n.built_year,
|
||||
n.property_type,
|
||||
n.metro_distance_m,
|
||||
n.school_quality,
|
||||
n.notes
|
||||
FROM silver.neighborhoods n
|
||||
JOIN silver.areas a ON a.area_id = n.area_id
|
||||
WHERE n.neighborhood_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(neighborhood_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::BadRequest("neighborhood not found".to_string()))?;
|
||||
|
||||
Ok(Json(neighborhood))
|
||||
}
|
||||
|
||||
pub async fn list_neighborhood_scores(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<NeighborhoodScoreQuery>,
|
||||
) -> ApiResult<Json<Vec<NeighborhoodScore>>> {
|
||||
validate_month(&query.month)?;
|
||||
|
||||
let scores = sqlx::query_as::<_, NeighborhoodScore>(
|
||||
r#"
|
||||
SELECT
|
||||
neighborhood_id,
|
||||
area_id,
|
||||
name,
|
||||
area_name,
|
||||
district,
|
||||
month,
|
||||
investment_score,
|
||||
recommendation,
|
||||
liquidity_score,
|
||||
rent_support_score,
|
||||
price_safety_score,
|
||||
location_score,
|
||||
building_age_score,
|
||||
transaction_count,
|
||||
transaction_price_psm,
|
||||
listing_count,
|
||||
listing_price_psm,
|
||||
rent_price_psm,
|
||||
median_days_on_market,
|
||||
annual_rent_yield_pct,
|
||||
discount_pct,
|
||||
available_units
|
||||
FROM gold.neighborhood_scores
|
||||
WHERE month = $1
|
||||
AND ($2::text IS NULL OR area_id = $2)
|
||||
ORDER BY investment_score DESC, neighborhood_id ASC
|
||||
"#,
|
||||
)
|
||||
.bind(query.month)
|
||||
.bind(query.area_id)
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
Ok(Json(scores))
|
||||
}
|
||||
|
||||
pub async fn list_watchlist_items(
|
||||
State(state): State<AppState>,
|
||||
) -> ApiResult<Json<Vec<WatchlistItem>>> {
|
||||
|
||||
Reference in New Issue
Block a user