Files
goodgo-platform/libs/ai-services/tests/test_avm_industrial.py
Ho Ngoc Hai 13bd76ac5d feat(ai-services): add building_coverage, loading_docks, zoning to industrial AVM
Completes the industrial-specific feature set required for AVM industrial
valuation. Adds heuristic adjustments for all three new features and
4 new tests covering zoning premiums, loading docks, and coverage ratio.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-16 17:06:27 +07:00

181 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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