"""Tests for industrial AVM rent estimation endpoint.""" from fastapi.testclient import TestClient from app.main import app client = TestClient(app) # ── Minimal valid request payload ─────────────────────────────── _PREDICT_PAYLOAD = { "province": "Bình Dương", "region": "south", "park_occupancy_rate": 0.85, "park_area_ha": 500, "park_age_years": 10, "distance_to_port_km": 60, "distance_to_airport_km": 30, "distance_to_highway_km": 5, "property_type": "factory", "area_m2": 5000, "ceiling_height_m": 10, "floor_load_ton_m2": 3.0, "power_capacity_kva": 1000, } def test_predict_industrial_heuristic(): """Predict using heuristic fallback (no trained model).""" resp = client.post("/avm/industrial/predict", json=_PREDICT_PAYLOAD) assert resp.status_code == 200 data = resp.json() assert data["estimated_rent_usd_m2"] > 0 assert 0 <= data["confidence"] <= 1 assert data["rent_range_low_usd_m2"] < data["estimated_rent_usd_m2"] assert data["rent_range_high_usd_m2"] > data["estimated_rent_usd_m2"] assert data["annual_rent_usd_m2"] > 0 assert data["total_monthly_rent_usd"] > 0 assert data["model_version"] == "heuristic-v1" def test_predict_industrial_returns_comparables(): """Heuristic should return comparable industrial properties.""" resp = client.post("/avm/industrial/predict", json=_PREDICT_PAYLOAD) data = resp.json() comps = data["comparables"] assert len(comps) > 0 for c in comps: assert c["park_name"] assert c["rent_usd_m2"] > 0 assert 0 <= c["similarity_score"] <= 1 def test_predict_industrial_returns_drivers(): """Heuristic should return feature importance drivers.""" resp = client.post("/avm/industrial/predict", json=_PREDICT_PAYLOAD) data = resp.json() drivers = data["drivers"] assert len(drivers) > 0 assert all(0 <= d["importance"] <= 1 for d in drivers) def test_predict_industrial_ready_built_premium(): """Ready-built factories should be priced higher than standard.""" standard = client.post("/avm/industrial/predict", json=_PREDICT_PAYLOAD).json() rbf_payload = {**_PREDICT_PAYLOAD, "property_type": "ready_built_factory"} ready_built = client.post("/avm/industrial/predict", json=rbf_payload).json() assert ready_built["estimated_rent_usd_m2"] > standard["estimated_rent_usd_m2"] def test_predict_industrial_open_yard_discount(): """Open yards should be cheaper than factories.""" factory = client.post("/avm/industrial/predict", json=_PREDICT_PAYLOAD).json() yard_payload = {**_PREDICT_PAYLOAD, "property_type": "open_yard"} yard = client.post("/avm/industrial/predict", json=yard_payload).json() assert yard["estimated_rent_usd_m2"] < factory["estimated_rent_usd_m2"] def test_predict_industrial_high_occupancy_premium(): """Higher park occupancy should increase rent.""" low = client.post( "/avm/industrial/predict", json={**_PREDICT_PAYLOAD, "park_occupancy_rate": 0.50}, ).json() high = client.post( "/avm/industrial/predict", json={**_PREDICT_PAYLOAD, "park_occupancy_rate": 0.95}, ).json() assert high["estimated_rent_usd_m2"] > low["estimated_rent_usd_m2"] def test_predict_industrial_annual_rent(): """Annual rent should be 12x monthly rent.""" resp = client.post("/avm/industrial/predict", json=_PREDICT_PAYLOAD).json() expected_annual = round(resp["estimated_rent_usd_m2"] * 12, 2) assert resp["annual_rent_usd_m2"] == expected_annual def test_predict_industrial_total_rent(): """Total monthly rent should be rent/m² × area.""" resp = client.post("/avm/industrial/predict", json=_PREDICT_PAYLOAD).json() expected_total = resp["estimated_rent_usd_m2"] * _PREDICT_PAYLOAD["area_m2"] assert abs(resp["total_monthly_rent_usd"] - expected_total) < 1.0 def test_predict_industrial_free_trade_zone_premium(): """Free-trade-zone zoning should command higher rent than general_industrial.""" general = client.post( "/avm/industrial/predict", json={**_PREDICT_PAYLOAD, "zoning": "general_industrial"}, ).json() ftz = client.post( "/avm/industrial/predict", json={**_PREDICT_PAYLOAD, "zoning": "free_trade_zone"}, ).json() assert ftz["estimated_rent_usd_m2"] > general["estimated_rent_usd_m2"] def test_predict_industrial_high_tech_zone_premium(): """High-tech zoning should command higher rent than general_industrial.""" general = client.post( "/avm/industrial/predict", json={**_PREDICT_PAYLOAD, "zoning": "general_industrial"}, ).json() ht = client.post( "/avm/industrial/predict", json={**_PREDICT_PAYLOAD, "zoning": "high_tech"}, ).json() assert ht["estimated_rent_usd_m2"] > general["estimated_rent_usd_m2"] def test_predict_industrial_loading_docks_premium(): """More loading docks should increase rent.""" no_docks = client.post( "/avm/industrial/predict", json={**_PREDICT_PAYLOAD, "loading_docks": 0}, ).json() many_docks = client.post( "/avm/industrial/predict", json={**_PREDICT_PAYLOAD, "loading_docks": 6}, ).json() assert many_docks["estimated_rent_usd_m2"] > no_docks["estimated_rent_usd_m2"] def test_predict_industrial_building_coverage_premium(): """Higher building coverage should increase rent.""" low_cov = client.post( "/avm/industrial/predict", json={**_PREDICT_PAYLOAD, "building_coverage": 0.3}, ).json() high_cov = client.post( "/avm/industrial/predict", json={**_PREDICT_PAYLOAD, "building_coverage": 0.7}, ).json() assert high_cov["estimated_rent_usd_m2"] > low_cov["estimated_rent_usd_m2"] def test_predict_industrial_validation_error(): """Missing required fields should return 422.""" resp = client.post("/avm/industrial/predict", json={"area_m2": 5000}) assert resp.status_code == 422 def test_predict_industrial_invalid_occupancy(): """Occupancy rate outside 0-1 should be rejected.""" resp = client.post( "/avm/industrial/predict", json={**_PREDICT_PAYLOAD, "park_occupancy_rate": 1.5}, ) assert resp.status_code == 422