feat: add neighborhood asset and scoring api

This commit is contained in:
2026-06-23 16:39:07 +08:00
parent c2604c65c1
commit b73a89e576
5 changed files with 327 additions and 4 deletions

View File

@@ -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>>> {