Spaces:
Running
Running
| """ | |
| Pytest fixtures untuk semua scenario tests. | |
| """ | |
| import sys | |
| import os | |
| from datetime import datetime, timedelta | |
| import pytest | |
| # Add project root ke path | |
| ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| sys.path.insert(0, ROOT) | |
| from matching_engine.models import ( | |
| Commodity, DemandNode, EmergencyMode, Kabupaten, LogisticsContext, | |
| SupplyNode, Tier, WeatherForecast, | |
| ) | |
| # ============================================================================= | |
| # Komoditas fixtures (subset untuk testing — 19 ada di constraints.py) | |
| # ============================================================================= | |
| def cabai_merah(): | |
| return Commodity( | |
| code="cabai_merah", nama="Cabai Merah Besar", | |
| max_distance_km=200, min_viable_tons=1.0, max_fresh_age_days=5, | |
| ) | |
| def bawang_merah(): | |
| return Commodity( | |
| code="bawang_merah", nama="Bawang Merah", | |
| max_distance_km=400, min_viable_tons=2.0, max_fresh_age_days=30, | |
| ) | |
| def beras_premium(): | |
| return Commodity( | |
| code="beras_premium", nama="Beras Premium", | |
| max_distance_km=800, min_viable_tons=5.0, max_fresh_age_days=180, | |
| ) | |
| def beras_medium(): | |
| return Commodity( | |
| code="beras_medium", nama="Beras Medium", | |
| max_distance_km=800, min_viable_tons=5.0, max_fresh_age_days=180, | |
| ) | |
| # ============================================================================= | |
| # Kabupaten fixtures — sample dari Jatim | |
| # ============================================================================= | |
| def surabaya(): | |
| """Tier 1, IPM 84.69, no equity boost. Pasar besar.""" | |
| return Kabupaten( | |
| id="3578", nama="Kota Surabaya", | |
| latitude=-7.2575, longitude=112.7521, | |
| ipm=84.69, tier=Tier.HIGH, | |
| ) | |
| def kediri_kab(): | |
| """Tier 2, IPM 74.50. Sentra cabai.""" | |
| return Kabupaten( | |
| id="3506", nama="Kediri", | |
| latitude=-7.7960, longitude=112.1700, | |
| ipm=74.50, tier=Tier.MEDIUM, | |
| ) | |
| def kota_kediri(): | |
| """Tier 1, IPM 81.48.""" | |
| return Kabupaten( | |
| id="3571", nama="Kota Kediri", | |
| latitude=-7.8166, longitude=112.0114, | |
| ipm=81.48, tier=Tier.HIGH, | |
| ) | |
| def sampang(): | |
| """Tier 2, IPM 66.72 — equity boost +30% (range <68, BPS 2024 kalibrasi).""" | |
| return Kabupaten( | |
| id="3527", nama="Sampang", | |
| latitude=-7.1924, longitude=113.2473, | |
| ipm=66.72, tier=Tier.MEDIUM, | |
| ) | |
| def sampang_severe(): | |
| """Hypothetical: kab dengan IPM jauh di bawah 68 untuk test threshold.""" | |
| return Kabupaten( | |
| id="9999", nama="Hypothetical-Severe", | |
| latitude=-7.1924, longitude=113.2473, | |
| ipm=62.0, tier=Tier.MEDIUM, | |
| ) | |
| def bangkalan(): | |
| """Tier 2, IPM 67.70 — equity boost +30% (range <68).""" | |
| return Kabupaten( | |
| id="3526", nama="Bangkalan", | |
| latitude=-7.0317, longitude=112.7491, | |
| ipm=67.70, tier=Tier.MEDIUM, | |
| ) | |
| def pamekasan(): | |
| """Tier 2, IPM 70.43 — equity boost +15% (range 68-72).""" | |
| return Kabupaten( | |
| id="3528", nama="Pamekasan", | |
| latitude=-7.1565, longitude=113.4785, | |
| ipm=70.43, tier=Tier.MEDIUM, | |
| ) | |
| def sumenep(): | |
| """Tier 1 IHK, IPM 68.79 — equity boost +15% (range 68-72).""" | |
| return Kabupaten( | |
| id="3529", nama="Sumenep", | |
| latitude=-7.0067, longitude=113.8525, | |
| ipm=68.79, tier=Tier.HIGH, | |
| ) | |
| def bondowoso(): | |
| """Tier 2, IPM 69.62 — equity boost +15% (range 68-72).""" | |
| return Kabupaten( | |
| id="3511", nama="Bondowoso", | |
| latitude=-7.9134, longitude=113.8217, | |
| ipm=69.62, tier=Tier.MEDIUM, | |
| ) | |
| def pacitan(): | |
| """Tier 2, IPM 71.40 — equity boost +15% (range 68-72). Pinggir barat Jatim.""" | |
| return Kabupaten( | |
| id="3501", nama="Pacitan", | |
| latitude=-8.1985, longitude=111.1014, | |
| ipm=71.40, tier=Tier.MEDIUM, | |
| ) | |
| def banyuwangi(): | |
| """Tier 1 IHK, IPM 73.45 — equity boost +5% (range 72-78). Pinggir timur Jatim.""" | |
| return Kabupaten( | |
| id="3510", nama="Banyuwangi", | |
| latitude=-8.2192, longitude=114.3691, | |
| ipm=73.45, tier=Tier.HIGH, | |
| ) | |
| def sidoarjo(): | |
| """Tier 2, IPM 80.13 — no equity boost. Pasar besar.""" | |
| return Kabupaten( | |
| id="3515", nama="Sidoarjo", | |
| latitude=-7.4549, longitude=112.7178, | |
| ipm=80.13, tier=Tier.MEDIUM, | |
| ) | |
| def gresik(): | |
| """Tier 2, IPM 77.61 — no equity boost.""" | |
| return Kabupaten( | |
| id="3525", nama="Gresik", | |
| latitude=-7.1568, longitude=112.6510, | |
| ipm=77.61, tier=Tier.MEDIUM, | |
| ) | |
| def blitar_kab(): | |
| """Tier 2, IPM 73.85 — equity boost +5%.""" | |
| return Kabupaten( | |
| id="3505", nama="Blitar", | |
| latitude=-8.0950, longitude=112.1658, | |
| ipm=73.85, tier=Tier.MEDIUM, | |
| ) | |
| def tulungagung(): | |
| """Tier 2, IPM 75.30 — no equity boost.""" | |
| return Kabupaten( | |
| id="3504", nama="Tulungagung", | |
| latitude=-8.0658, longitude=111.9028, | |
| ipm=75.30, tier=Tier.MEDIUM, | |
| ) | |
| def trenggalek(): | |
| """Tier 2, IPM 73.85 — equity boost +5%.""" | |
| return Kabupaten( | |
| id="3503", nama="Trenggalek", | |
| latitude=-8.0497, longitude=111.7100, | |
| ipm=73.85, tier=Tier.MEDIUM, | |
| ) | |
| def probolinggo_kab(): | |
| """Tier 2, IPM 69.40 — equity boost +15%.""" | |
| return Kabupaten( | |
| id="3513", nama="Probolinggo", | |
| latitude=-7.8742, longitude=113.4675, | |
| ipm=69.40, tier=Tier.MEDIUM, | |
| ) | |
| def lumajang(): | |
| """Tier 2, IPM 70.10 — equity boost +5%.""" | |
| return Kabupaten( | |
| id="3508", nama="Lumajang", | |
| latitude=-8.1336, longitude=113.2245, | |
| ipm=70.10, tier=Tier.MEDIUM, | |
| ) | |
| # ============================================================================= | |
| # Logistics & weather fixtures | |
| # ============================================================================= | |
| def logistics_normal(): | |
| return LogisticsContext( | |
| bbm_price_idr_per_liter=10000, | |
| bbm_price_baseline=10000, | |
| ) | |
| def logistics_bbm_naik_20pct(): | |
| """Skenario E5: BBM naik 20%.""" | |
| return LogisticsContext( | |
| bbm_price_idr_per_liter=12000, | |
| bbm_price_baseline=10000, | |
| ) | |
| def logistics_ramadan(): | |
| """Skenario C1: pre-Ramadan H-14.""" | |
| return LogisticsContext(is_ramadan_proximity=True) | |
| def weather_clear(): | |
| """Cuaca cerah default — climate score = 1.0.""" | |
| return {} # no forecast = default 0.7 fallback or specific routes | |
| def weather_banjir(): | |
| """Skenario D1: hujan deras 75mm di rute Kediri-Surabaya.""" | |
| return { | |
| "3506_3578": WeatherForecast( | |
| origin_kab_id="3506", dest_kab_id="3578", | |
| max_rain_mm=75.0, transit_window_days=1, | |
| ), | |
| "3571_3578": WeatherForecast( | |
| origin_kab_id="3571", dest_kab_id="3578", | |
| max_rain_mm=75.0, transit_window_days=1, | |
| ), | |
| } | |
| # ============================================================================= | |
| # Helper untuk membuat SupplyNode/DemandNode dengan defaults sane | |
| # ============================================================================= | |
| def make_supply(): | |
| """Factory fixture untuk membuat SupplyNode.""" | |
| def _make(kab, commodity, volume=50.0, price=40000, age=2): | |
| return SupplyNode( | |
| kabupaten=kab, commodity=commodity, | |
| volume_tons=volume, price_per_kg=price, | |
| harvest_age_days=age, | |
| ) | |
| return _make | |
| def make_demand(): | |
| """Factory fixture untuk membuat DemandNode.""" | |
| def _make(kab, commodity, volume=50.0, price=60000): | |
| return DemandNode( | |
| kabupaten=kab, commodity=commodity, | |
| volume_tons=volume, price_per_kg=price, | |
| ) | |
| return _make | |
| def make_stale_supply(): | |
| """Factory untuk SupplyNode dengan timestamp lama (skenario C3).""" | |
| def _make(kab, commodity, hours_old=48, volume=50.0, price=40000, age=2): | |
| return SupplyNode( | |
| kabupaten=kab, commodity=commodity, | |
| volume_tons=volume, price_per_kg=price, | |
| harvest_age_days=age, | |
| timestamp=datetime.now() - timedelta(hours=hours_old), | |
| ) | |
| return _make | |