feat: add area detail workspace
This commit is contained in:
@@ -5,13 +5,13 @@ use serde::Serialize;
|
||||
use crate::error::{ApiError, ApiResult};
|
||||
use crate::import_templates::{execute_import_payload, validate_import_payload};
|
||||
use crate::models::{
|
||||
AreaScore, AreaScoreLineage, AreaScoreLineageQuery, AreaScoreModelRunResponse,
|
||||
AreaScoreSummary, CreateAreaScoreModelRun, CreateDataSource, CreateIngestionRun,
|
||||
CreateRawArtifact, CreateWatchlistItem, DataSource, FinishIngestionRun, ImportExecutionRequest,
|
||||
ImportExecutionResponse, ImportValidationRequest, ImportValidationResponse, IngestionRun,
|
||||
IngestionRunQuery, MarketOverview, ModelRun, MonthQuery, Neighborhood, NeighborhoodQuery,
|
||||
NeighborhoodScore, NeighborhoodScoreQuery, RawArtifact, RawArtifactQuery, UpdateWatchlistItem,
|
||||
WatchlistItem,
|
||||
AreaDetail, AreaMonthlyMetric, AreaProfile, AreaScore, AreaScoreLineage, AreaScoreLineageQuery,
|
||||
AreaScoreModelRunResponse, AreaScoreSummary, CreateAreaScoreModelRun, CreateDataSource,
|
||||
CreateIngestionRun, CreateRawArtifact, CreateWatchlistItem, DataSource, FinishIngestionRun,
|
||||
ImportExecutionRequest, ImportExecutionResponse, ImportValidationRequest,
|
||||
ImportValidationResponse, IngestionRun, IngestionRunQuery, MarketOverview, ModelRun,
|
||||
MonthQuery, Neighborhood, NeighborhoodQuery, NeighborhoodScore, NeighborhoodScoreQuery,
|
||||
RawArtifact, RawArtifactQuery, UpdateWatchlistItem, WatchlistItem,
|
||||
};
|
||||
use crate::scoring::{compute_area_scores, previous_month, AreaMetricRow, ComputedAreaScore};
|
||||
use crate::state::AppState;
|
||||
@@ -233,7 +233,19 @@ pub async fn get_area_score_lineage(
|
||||
Query(query): Query<AreaScoreLineageQuery>,
|
||||
) -> ApiResult<Json<AreaScoreLineage>> {
|
||||
validate_month(&query.month)?;
|
||||
let lineage = sqlx::query_as::<_, AreaScoreLineage>(
|
||||
let lineage = fetch_area_score_lineage(&state, &area_id, &query.month)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::BadRequest("area score lineage not found".to_string()))?;
|
||||
|
||||
Ok(Json(lineage))
|
||||
}
|
||||
|
||||
async fn fetch_area_score_lineage(
|
||||
state: &AppState,
|
||||
area_id: &str,
|
||||
month: &str,
|
||||
) -> Result<Option<AreaScoreLineage>, sqlx::Error> {
|
||||
sqlx::query_as::<_, AreaScoreLineage>(
|
||||
r#"
|
||||
SELECT
|
||||
s.area_id,
|
||||
@@ -261,12 +273,151 @@ pub async fn get_area_score_lineage(
|
||||
"#,
|
||||
)
|
||||
.bind(area_id)
|
||||
.bind(query.month)
|
||||
.bind(month)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_area_detail(
|
||||
State(state): State<AppState>,
|
||||
Path(area_id): Path<String>,
|
||||
Query(query): Query<MonthQuery>,
|
||||
) -> ApiResult<Json<AreaDetail>> {
|
||||
validate_month(&query.month)?;
|
||||
|
||||
let profile = sqlx::query_as::<_, AreaProfile>(
|
||||
r#"
|
||||
SELECT area_id, name, district, segment, notes
|
||||
FROM silver.areas
|
||||
WHERE area_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&area_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::BadRequest("area score lineage not found".to_string()))?;
|
||||
.ok_or_else(|| ApiError::BadRequest("area not found".to_string()))?;
|
||||
|
||||
Ok(Json(lineage))
|
||||
let current_score = sqlx::query_as::<_, AreaScore>(
|
||||
r#"
|
||||
SELECT
|
||||
area_id,
|
||||
name,
|
||||
district,
|
||||
segment,
|
||||
month,
|
||||
investment_score,
|
||||
recommendation,
|
||||
liquidity_score,
|
||||
momentum_score,
|
||||
rent_support_score,
|
||||
safety_margin_score,
|
||||
credit_support_score,
|
||||
supply_risk_score,
|
||||
valuation_pressure_score,
|
||||
transaction_count,
|
||||
transaction_price_psm,
|
||||
listing_count,
|
||||
listing_price_psm,
|
||||
rent_price_psm,
|
||||
median_days_on_market,
|
||||
annual_rent_yield_pct,
|
||||
listing_pressure_ratio,
|
||||
discount_pct,
|
||||
price_momentum_pct,
|
||||
volume_momentum_pct
|
||||
FROM gold.area_scores
|
||||
WHERE area_id = $1
|
||||
AND month = $2
|
||||
"#,
|
||||
)
|
||||
.bind(&area_id)
|
||||
.bind(&query.month)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
|
||||
let score_lineage = fetch_area_score_lineage(&state, &area_id, &query.month).await?;
|
||||
let monthly_metrics = sqlx::query_as::<_, AreaMonthlyMetric>(
|
||||
r#"
|
||||
SELECT
|
||||
area_id,
|
||||
month,
|
||||
transaction_count,
|
||||
transaction_price_psm,
|
||||
listing_count,
|
||||
listing_price_psm,
|
||||
median_days_on_market,
|
||||
rent_price_psm,
|
||||
new_supply_units,
|
||||
land_residential_gfa_sqm,
|
||||
mortgage_rate_pct,
|
||||
policy_signal,
|
||||
source,
|
||||
ingestion_run_id,
|
||||
raw_artifact_id
|
||||
FROM silver.area_monthly_metrics
|
||||
WHERE area_id = $1
|
||||
AND month <= $2
|
||||
ORDER BY month DESC
|
||||
LIMIT 12
|
||||
"#,
|
||||
)
|
||||
.bind(&area_id)
|
||||
.bind(&query.month)
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
let neighborhood_scores =
|
||||
fetch_neighborhood_scores_for_area(&state, &area_id, &query.month).await?;
|
||||
|
||||
Ok(Json(AreaDetail {
|
||||
profile,
|
||||
current_score,
|
||||
score_lineage,
|
||||
monthly_metrics,
|
||||
neighborhood_scores,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn fetch_neighborhood_scores_for_area(
|
||||
state: &AppState,
|
||||
area_id: &str,
|
||||
month: &str,
|
||||
) -> Result<Vec<NeighborhoodScore>, sqlx::Error> {
|
||||
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 area_id = $1
|
||||
AND month = $2
|
||||
ORDER BY investment_score DESC, neighborhood_id ASC
|
||||
"#,
|
||||
)
|
||||
.bind(area_id)
|
||||
.bind(month)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_neighborhoods(
|
||||
|
||||
Reference in New Issue
Block a user