From 4c3443c2d621b4ce37e17a9925a4e214fab4c5f3 Mon Sep 17 00:00:00 2001 From: arifal hidayat Date: Wed, 18 Jun 2025 22:53:44 +0700 Subject: [PATCH] restructure retribution calculations table --- OPTIMIZED_TABLE_STRUCTURE.md | 210 +++++++++++++ .../Commands/TestRetributionCalculation.php | 263 ++++++++++++++++ app/Console/Commands/TestRetributionData.php | 291 ++++++++++++++++++ app/Models/BuildingType.php | 131 ++++++++ app/Models/HeightIndex.php | 55 ++++ app/Models/RetributionCalculation.php | 81 +++++ app/Models/RetributionConfig.php | 50 +++ app/Models/RetributionIndex.php | 57 ++++ app/Services/RetributionCalculatorService.php | 243 +++++++++++++++ ...11300_create_new_retribution_structure.php | 95 ++++++ database/seeders/DatabaseSeeder.php | 1 + database/seeders/RetributionDataSeeder.php | 71 +++++ 12 files changed, 1548 insertions(+) create mode 100644 OPTIMIZED_TABLE_STRUCTURE.md create mode 100644 app/Console/Commands/TestRetributionCalculation.php create mode 100644 app/Console/Commands/TestRetributionData.php create mode 100644 app/Models/BuildingType.php create mode 100644 app/Models/HeightIndex.php create mode 100644 app/Models/RetributionCalculation.php create mode 100644 app/Models/RetributionConfig.php create mode 100644 app/Models/RetributionIndex.php create mode 100644 app/Services/RetributionCalculatorService.php create mode 100644 database/migrations/2025_06_18_211300_create_new_retribution_structure.php create mode 100644 database/seeders/RetributionDataSeeder.php diff --git a/OPTIMIZED_TABLE_STRUCTURE.md b/OPTIMIZED_TABLE_STRUCTURE.md new file mode 100644 index 0000000..bcf9623 --- /dev/null +++ b/OPTIMIZED_TABLE_STRUCTURE.md @@ -0,0 +1,210 @@ +# Struktur Tabel Retribusi PBG yang Dioptimalkan + +## Ringkasan Optimasi + +Struktur tabel baru ini **lebih sederhana**, **fokus pada perhitungan**, dan **menghilangkan redundansi** dari struktur sebelumnya. + +## Perbandingan Struktur + +### SEBELUM (Kompleks) + +- `building_functions` - 8 kolom + relationship kompleks +- `building_function_parameters` - 12 kolom dengan mismatch model/migration +- `retribution_formulas` - Menyimpan formula sebagai string +- `retribution_proposals` - 15+ kolom dengan banyak redundansi +- `floor_height_indices` - OK, tidak berubah + +### SESUDAH (Sederhana) + +- `building_types` - **7 kolom**, hierarki sederhana +- `retribution_indices` - **6 kolom**, parameter calculation saja +- `height_indices` - **3 kolom**, sama seperti sebelumnya +- `retribution_configs` - **5 kolom**, konfigurasi global +- `retribution_calculations` - **8 kolom**, hasil perhitungan saja + +--- + +## Detail Struktur Tabel Baru + +### 1. `building_types` + +**Fungsi:** Menyimpan jenis fungsi bangunan dengan hierarki sederhana + +| Kolom | Tipe | Keterangan | +| ------------- | ------------ | ---------------------------------- | +| `id` | bigint | Primary key | +| `code` | varchar(10) | Kode unik (UMKM, KEAGAMAAN, dll) | +| `name` | varchar(100) | Nama fungsi bangunan | +| `parent_id` | bigint | ID parent (untuk hierarki) | +| `level` | tinyint | Level hierarki (1=parent, 2=child) | +| `coefficient` | decimal(8,4) | **Koefisien untuk perhitungan** | +| `is_free` | boolean | **Apakah gratis (keagamaan, MBR)** | + +### 2. `retribution_indices` + +**Fungsi:** Menyimpan parameter indeks untuk perhitungan (1:1 dengan building_types) + +| Kolom | Tipe | Keterangan | +| ----------------------- | ------------ | ---------------------------------- | +| `id` | bigint | Primary key | +| `building_type_id` | bigint | FK ke building_types | +| `ip_permanent` | decimal(8,4) | **Indeks Permanensi** | +| `ip_complexity` | decimal(8,4) | **Indeks Kompleksitas** | +| `locality_index` | decimal(8,4) | **Indeks Lokalitas** | +| `infrastructure_factor` | decimal(8,4) | **Faktor prasarana (default 50%)** | + +### 3. `height_indices` + +**Fungsi:** Indeks ketinggian per lantai (sama seperti sebelumnya) + +| Kolom | Tipe | Keterangan | +| -------------- | ------------ | ---------------------------------------- | +| `id` | bigint | Primary key | +| `floor_number` | tinyint | Nomor lantai (1,2,3,4,5,6) | +| `height_index` | decimal(8,6) | **IP Ketinggian (1.0, 1.09, 1.12, dst)** | + +### 4. `retribution_configs` + +**Fungsi:** Konfigurasi global untuk perhitungan (menggantikan hard-coded values) + +| Kolom | Tipe | Keterangan | +| ------------- | ------------- | --------------------- | +| `id` | bigint | Primary key | +| `key` | varchar(50) | Kunci konfigurasi | +| `value` | decimal(15,2) | **Nilai konfigurasi** | +| `description` | varchar(200) | Deskripsi | + +**Data yang disimpan:** + +- `BASE_VALUE` = 70350 (nilai dasar) +- `INFRASTRUCTURE_MULTIPLIER` = 0.5 (50% prasarana) +- `HEIGHT_MULTIPLIER` = 0.5 (pengali indeks ketinggian) + +### 5. `retribution_calculations` + +**Fungsi:** Hasil perhitungan retribusi (history) + +| Kolom | Tipe | Keterangan | +| -------------------- | ------------- | -------------------------------- | +| `id` | bigint | Primary key | +| `calculation_id` | varchar(20) | ID unik perhitungan | +| `building_type_id` | bigint | FK ke building_types | +| `floor_number` | tinyint | Lantai yang dipilih | +| `building_area` | decimal(12,2) | **Luas bangunan input** | +| `retribution_amount` | decimal(15,2) | **Hasil perhitungan** | +| `calculation_detail` | json | **Detail breakdown perhitungan** | +| `calculated_at` | timestamp | Waktu perhitungan | + +--- + +## Formula Perhitungan + +### Formula Excel yang Diimplementasikan: + +``` +H13 = coefficient * (ip_permanent + ip_complexity + (0.5 * height_index)) + +Main Calculation = building_area * (locality_index * BASE_VALUE * H13) + +Infrastructure = INFRASTRUCTURE_MULTIPLIER * Main Calculation + +Total Retribution = Main Calculation + Infrastructure +``` + +### Implementasi dalam Service: + +```php +// Step 1: Calculate H13 coefficient +$h13 = $buildingType->coefficient * ( + $indices->ip_permanent + + $indices->ip_complexity + + (0.5 * $heightIndex) +); + +// Step 2: Main calculation +$mainCalculation = $buildingArea * ($indices->locality_index * $baseValue * $h13); + +// Step 3: Infrastructure (50% additional) +$infrastructureCalculation = 0.5 * $mainCalculation; + +// Step 4: Total +$totalRetribution = $mainCalculation + $infrastructureCalculation; +``` + +--- + +## Keuntungan Struktur Baru + +### ✅ **Simplicity** + +- **5 tabel** vs 8+ tabel sebelumnya +- **Kolom minimal** hanya yang diperlukan untuk perhitungan +- **No redundant data** seperti ip_ketinggian di proposals + +### ✅ **Performance** + +- **Proper indexes** untuk query yang sering digunakan +- **Normalized structure** mengurangi storage +- **Cached configs** untuk values yang jarang berubah + +### ✅ **Maintainability** + +- **Clear separation** antara master data dan calculation results +- **Configurable values** tidak hard-coded lagi +- **Single responsibility** setiap tabel punya tujuan jelas + +### ✅ **Flexibility** + +- **Easy to extend** untuk fungsi bangunan baru +- **Configurable formulas** lewat RetributionConfig +- **Audit trail** lewat calculation history + +### ✅ **Data Integrity** + +- **Proper constraints** untuk validasi data +- **Foreign key relationships** yang benar +- **No model-migration mismatch** + +--- + +## Migration Guide + +### Langkah Implementasi: + +1. **Run Migration:** `php artisan migrate` untuk tabel baru +2. **Seed Data:** Data master berdasarkan Excel akan otomatis ter-seed +3. **Update Code:** Ganti penggunaan model lama dengan model baru +4. **Test Calculation:** Verifikasi hasil perhitungan sama dengan Excel +5. **Deploy:** Struktur siap production + +### Data Migration (Optional): + +Jika ada data existing di tabel lama yang perlu dipindahkan, buat script migration untuk transfer data dari struktur lama ke struktur baru. + +--- + +## Usage Example + +```php +// Initialize service +$calculator = new RetributionCalculatorService(); + +// Calculate retribution +$result = $calculator->calculate( + buildingTypeId: 8, // UMKM + floorNumber: 2, // 2 lantai + buildingArea: 100.50, // 100.5 m2 + saveResult: true // Simpan ke database +); + +// Result structure +[ + 'building_type' => [...], + 'total_retribution' => 31658.25, + 'formatted_amount' => 'Rp 31,658.25', + 'calculation_steps' => [...], + 'calculation_id' => 'RTB-20250130140530-123' +] +``` + +Struktur ini **jauh lebih clean**, **mudah dipahami**, dan **optimal untuk perhitungan retribusi PBG**! diff --git a/app/Console/Commands/TestRetributionCalculation.php b/app/Console/Commands/TestRetributionCalculation.php new file mode 100644 index 0000000..88fd30b --- /dev/null +++ b/app/Console/Commands/TestRetributionCalculation.php @@ -0,0 +1,263 @@ +calculatorService = $calculatorService; + } + + /** + * Execute the console command. + */ + public function handle() + { + $this->info('🏢 SISTEM TEST PERHITUNGAN RETRIBUSI PBG'); + $this->info('=' . str_repeat('=', 50)); + + // Test all building types if --all flag is used + if ($this->option('all')) { + return $this->testAllBuildingTypes(); + } + + // Get input parameters + $area = $this->getArea(); + $floor = $this->getFloor(); + $buildingTypeId = $this->getBuildingType(); + + if (!$area || !$floor || !$buildingTypeId) { + $this->error('❌ Parameter tidak lengkap!'); + return 1; + } + + // Perform calculation + $this->performCalculation($buildingTypeId, $floor, $area); + + return 0; + } + + protected function getArea() + { + $area = $this->option('area'); + + if (!$area) { + $area = $this->ask('📐 Masukkan luas bangunan (m²)'); + } + + if (!is_numeric($area) || $area <= 0) { + $this->error('❌ Luas bangunan harus berupa angka positif!'); + return null; + } + + return (float) $area; + } + + protected function getFloor() + { + $floor = $this->option('floor'); + + if (!$floor) { + $floor = $this->ask('🏗️ Masukkan jumlah lantai (1-6)'); + } + + if (!is_numeric($floor) || $floor < 1 || $floor > 6) { + $this->error('❌ Jumlah lantai harus antara 1-6!'); + return null; + } + + return (int) $floor; + } + + protected function getBuildingType() + { + $type = $this->option('type'); + + if (!$type) { + $this->showBuildingTypes(); + $type = $this->ask('🏢 Masukkan ID atau kode building type'); + } + + // Try to find by ID first, then by code + $buildingType = null; + + if (is_numeric($type)) { + $buildingType = BuildingType::find($type); + } else { + $buildingType = BuildingType::where('code', strtoupper($type))->first(); + } + + if (!$buildingType) { + $this->error('❌ Building type tidak ditemukan!'); + return null; + } + + return $buildingType->id; + } + + protected function showBuildingTypes() + { + $this->info('📋 DAFTAR BUILDING TYPES:'); + $this->line(''); + + $buildingTypes = BuildingType::with('indices') + ->whereHas('indices') // Only types that have indices + ->get(); + + $headers = ['ID', 'Kode', 'Nama', 'Coefficient', 'Free']; + $rows = []; + + foreach ($buildingTypes as $type) { + $rows[] = [ + $type->id, + $type->code, + $type->name, + $type->indices ? number_format($type->indices->coefficient, 4) : 'N/A', + $type->is_free ? '✅' : '❌' + ]; + } + + $this->table($headers, $rows); + $this->line(''); + } + + protected function performCalculation($buildingTypeId, $floor, $area) + { + try { + $result = $this->calculatorService->calculate($buildingTypeId, $floor, $area, false); + + $this->displayResults($result, $area, $floor); + + } catch (\Exception $e) { + $this->error('❌ Error: ' . $e->getMessage()); + return 1; + } + } + + protected function displayResults($result, $area, $floor) + { + $this->info(''); + $this->info('📊 HASIL PERHITUNGAN RETRIBUSI'); + $this->info('=' . str_repeat('=', 40)); + + // Building info + $this->line('🏢 Building Type: ' . $result['building_type']['name']); + $this->line('📐 Luas Bangunan: ' . number_format($area, 0) . ' m²'); + $this->line('🏗️ Jumlah Lantai: ' . $floor); + + if (isset($result['building_type']['is_free']) && $result['building_type']['is_free']) { + $this->line(''); + $this->info('🎉 GRATIS - Building type ini tidak dikenakan retribusi'); + $this->line('💰 Total Retribusi: Rp 0'); + return; + } + + $this->line(''); + + // Parameters + $this->info('📋 PARAMETER PERHITUNGAN:'); + $indices = $result['indices']; + $this->line('• Coefficient: ' . number_format($indices['coefficient'], 4)); + $this->line('• IP Permanent: ' . number_format($indices['ip_permanent'], 4)); + $this->line('• IP Complexity: ' . number_format($indices['ip_complexity'], 4)); + $this->line('• Locality Index: ' . number_format($indices['locality_index'], 4)); + $this->line('• Height Index: ' . number_format($result['input_parameters']['height_index'], 4)); + + $this->line(''); + + // Calculation steps + $this->info('🔢 LANGKAH PERHITUNGAN:'); + $detail = $result['calculation_detail']; + $this->line('1. H5 Raw: ' . number_format($detail['h5_raw'], 6)); + $this->line('2. H5 Rounded: ' . number_format($detail['h5'], 4)); + $this->line('3. Main Calculation: Rp ' . number_format($detail['main'], 2)); + $this->line('4. Infrastructure (50%): Rp ' . number_format($detail['infrastructure'], 2)); + + $this->line(''); + + // Final result + $this->info('💰 TOTAL RETRIBUSI: ' . $result['formatted_amount'] . ''); + $this->line('📈 Per m²: Rp ' . number_format($result['total_retribution'] / $area, 2) . ''); + } + + protected function testAllBuildingTypes() + { + $area = $this->option('area') ?: 100; + $floor = $this->option('floor') ?: 2; + + $this->info("🧪 TESTING SEMUA BUILDING TYPES"); + $this->info("📐 Luas: {$area} m² | 🏗️ Lantai: {$floor}"); + $this->info('=' . str_repeat('=', 60)); + + $buildingTypes = BuildingType::with('indices') + ->whereHas('indices') // Only types that have indices + ->orderBy('level') + ->orderBy('name') + ->get(); + + $headers = ['Kode', 'Nama', 'Coefficient', 'Total Retribusi', 'Per m²']; + $rows = []; + + foreach ($buildingTypes as $type) { + try { + $result = $this->calculatorService->calculate($type->id, $floor, $area, false); + + if ($type->is_free) { + $rows[] = [ + $type->code, + $type->name, + 'FREE', + 'Rp 0', + 'Rp 0' + ]; + } else { + $rows[] = [ + $type->code, + $type->name, + number_format($result['indices']['coefficient'], 4), + 'Rp ' . number_format($result['total_retribution'], 0), + 'Rp ' . number_format($result['total_retribution'] / $area, 0) + ]; + } + } catch (\Exception $e) { + $rows[] = [ + $type->code, + $type->name, + 'ERROR', + $e->getMessage(), + '-' + ]; + } + } + + $this->table($headers, $rows); + + return 0; + } +} diff --git a/app/Console/Commands/TestRetributionData.php b/app/Console/Commands/TestRetributionData.php new file mode 100644 index 0000000..cf64150 --- /dev/null +++ b/app/Console/Commands/TestRetributionData.php @@ -0,0 +1,291 @@ +calculatorService = $calculatorService; + } + + public function handle() + { + $this->info('🗄️ SISTEM TEST DATA & RELASI RETRIBUSI PBG'); + $this->info('=' . str_repeat('=', 55)); + + if ($this->option('clear')) { + $this->clearCalculationHistory(); + return; + } + + if ($this->option('show')) { + $this->showExistingData(); + return; + } + + if ($this->option('save')) { + $this->saveTestCalculations(); + } + + $this->showDatabaseStructure(); + $this->showSampleData(); + $this->showRelations(); + } + + protected function saveTestCalculations() + { + $this->info('💾 MENYIMPAN SAMPLE CALCULATIONS...'); + $this->line(''); + + $testCases = [ + ['type_code' => 'KEAGAMAAN', 'area' => 200, 'floor' => 1], + ['type_code' => 'SOSBUDAYA', 'area' => 150, 'floor' => 2], + ['type_code' => 'CAMP_KECIL', 'area' => 1, 'floor' => 1], + ['type_code' => 'UMKM', 'area' => 100, 'floor' => 2], + ['type_code' => 'HUN_SEDH', 'area' => 80, 'floor' => 1], + ['type_code' => 'USH_BESAR', 'area' => 500, 'floor' => 3], + ]; + + foreach ($testCases as $case) { + $buildingType = BuildingType::where('code', $case['type_code'])->first(); + + if (!$buildingType) { + $this->warn("⚠️ Building type {$case['type_code']} not found"); + continue; + } + + $result = $this->calculatorService->calculate( + $buildingType->id, + $case['floor'], + $case['area'] + ); + + // Save to database + RetributionCalculation::create([ + 'calculation_id' => 'TST' . now()->format('ymdHis') . rand(10, 99), + 'building_type_id' => $buildingType->id, + 'floor_number' => $case['floor'], + 'building_area' => $case['area'], + 'retribution_amount' => $result['total_retribution'], + 'calculation_detail' => json_encode($result), + 'calculated_at' => now() + ]); + + $this->info("✅ Saved: {$buildingType->name} - {$case['area']}m² - {$case['floor']} lantai - Rp " . number_format($result['total_retribution'])); + } + + $this->line(''); + $this->info('💾 Sample calculations saved successfully!'); + $this->line(''); + } + + protected function showExistingData() + { + $this->info('📊 DATA YANG TERSIMPAN DI DATABASE'); + $this->info('=' . str_repeat('=', 40)); + + $calculations = RetributionCalculation::with('buildingType') + ->orderBy('created_at', 'desc') + ->limit(10) + ->get(); + + if ($calculations->isEmpty()) { + $this->warn('❌ Tidak ada data calculation yang tersimpan'); + $this->info('💡 Gunakan --save untuk menyimpan sample data'); + return; + } + + $headers = ['ID', 'Building Type', 'Area', 'Floor', 'Amount', 'Created']; + $rows = []; + + foreach ($calculations as $calc) { + $rows[] = [ + substr($calc->calculation_id, -8), + $calc->buildingType->name ?? 'N/A', + $calc->building_area . ' m²', + $calc->floor_number, + 'Rp ' . number_format($calc->retribution_amount), + $calc->created_at->format('d/m H:i') + ]; + } + + $this->table($headers, $rows); + } + + protected function clearCalculationHistory() + { + $count = RetributionCalculation::count(); + + if ($count === 0) { + $this->info('ℹ️ Tidak ada data calculation untuk dihapus'); + return; + } + + if ($this->confirm("🗑️ Hapus {$count} calculation records?")) { + RetributionCalculation::truncate(); + $this->info("✅ {$count} calculation records berhasil dihapus"); + } + } + + protected function showDatabaseStructure() + { + $this->info('🏗️ STRUKTUR DATABASE RETRIBUSI'); + $this->info('=' . str_repeat('=', 35)); + + $tables = [ + 'building_types' => 'Hierarki dan metadata building types', + 'retribution_indices' => 'Parameter perhitungan (coefficient, IP, dll)', + 'height_indices' => 'Koefisien tinggi berdasarkan lantai', + 'retribution_configs' => 'Konfigurasi global (base value, dll)', + 'retribution_calculations' => 'History perhitungan dan hasil' + ]; + + foreach ($tables as $table => $description) { + $this->line("📋 {$table}: {$description}"); + } + + $this->line(''); + } + + protected function showSampleData() + { + $this->info('📋 SAMPLE DATA DARI SETIAP TABEL'); + $this->info('=' . str_repeat('=', 35)); + + // Building Types + $this->line('🏢 BUILDING TYPES:'); + $buildingTypes = BuildingType::select('id', 'code', 'name', 'level', 'is_free') + ->orderBy('level') + ->orderBy('name') + ->get(); + + $headers = ['ID', 'Code', 'Name', 'Level', 'Free']; + $rows = []; + foreach ($buildingTypes->take(5) as $type) { + $rows[] = [ + $type->id, + $type->code, + substr($type->name, 0, 25) . '...', + $type->level, + $type->is_free ? '✅' : '❌' + ]; + } + $this->table($headers, $rows); + + // Retribution Indices + $this->line('📊 RETRIBUTION INDICES:'); + $indices = RetributionIndex::with('buildingType') + ->select('building_type_id', 'coefficient', 'ip_permanent', 'ip_complexity', 'locality_index') + ->get(); + + $headers = ['Building Type', 'Coefficient', 'IP Permanent', 'IP Complexity', 'Locality']; + $rows = []; + foreach ($indices->take(5) as $index) { + $rows[] = [ + $index->buildingType->code ?? 'N/A', + number_format($index->coefficient, 4), + number_format($index->ip_permanent, 4), + number_format($index->ip_complexity, 4), + number_format($index->locality_index, 4) + ]; + } + $this->table($headers, $rows); + + // Height Indices + $this->line('🏗️ HEIGHT INDICES:'); + $heights = HeightIndex::orderBy('floor_number')->get(); + + $headers = ['Floor', 'Height Index']; + $rows = []; + foreach ($heights as $height) { + $rows[] = [ + $height->floor_number, + number_format($height->height_index, 4) + ]; + } + $this->table($headers, $rows); + + // Retribution Configs + $this->line('⚙️ RETRIBUTION CONFIGS:'); + $configs = RetributionConfig::all(); + + $headers = ['Key', 'Value', 'Description']; + $rows = []; + foreach ($configs as $config) { + $rows[] = [ + $config->key, + $config->value, + substr($config->description ?? '', 0, 30) . '...' + ]; + } + $this->table($headers, $rows); + } + + protected function showRelations() + { + $this->info('🔗 RELASI ANTAR TABEL'); + $this->info('=' . str_repeat('=', 25)); + + // Test relations dengan sample data + $buildingType = BuildingType::with(['indices', 'calculations']) + ->where('code', 'UMKM') + ->first(); + + if (!$buildingType) { + $this->warn('⚠️ Sample building type tidak ditemukan'); + return; + } + + $this->line("🏢 Building Type: {$buildingType->name}"); + $this->line(" 📋 Code: {$buildingType->code}"); + $this->line(" 📊 Level: {$buildingType->level}"); + $this->line(" 🆓 Free: " . ($buildingType->is_free ? 'Ya' : 'Tidak')); + + // Show indices relation + if ($buildingType->indices) { + $index = $buildingType->indices; + $this->line(" 📊 Retribution Index:"); + $this->line(" 💰 Coefficient: " . number_format($index->coefficient, 4)); + $this->line(" 🏗️ IP Permanent: " . number_format($index->ip_permanent, 4)); + $this->line(" 🔧 IP Complexity: " . number_format($index->ip_complexity, 4)); + $this->line(" 📍 Locality Index: " . number_format($index->locality_index, 4)); + } + + // Show calculations relation + $calculationsCount = $buildingType->calculations()->count(); + $this->line(" 📈 Calculations: {$calculationsCount} records"); + + if ($calculationsCount > 0) { + $latestCalc = $buildingType->calculations()->latest()->first(); + $this->line(" 📅 Latest: " . $latestCalc->created_at->format('d/m/Y H:i')); + $this->line(" 💰 Amount: Rp " . number_format($latestCalc->retribution_amount)); + } + + $this->line(''); + $this->info('🎯 KESIMPULAN RELASI:'); + $this->line(' • BuildingType hasOne RetributionIndex'); + $this->line(' • BuildingType hasMany RetributionCalculations'); + $this->line(' • RetributionCalculation belongsTo BuildingType'); + $this->line(' • HeightIndex independent (digunakan berdasarkan floor_number)'); + $this->line(' • RetributionConfig global settings'); + } +} \ No newline at end of file diff --git a/app/Models/BuildingType.php b/app/Models/BuildingType.php new file mode 100644 index 0000000..2de536e --- /dev/null +++ b/app/Models/BuildingType.php @@ -0,0 +1,131 @@ + 'integer', + 'is_free' => 'boolean', + 'is_active' => 'boolean' + ]; + + /** + * Parent relationship + */ + public function parent(): BelongsTo + { + return $this->belongsTo(BuildingType::class, 'parent_id'); + } + + /** + * Children relationship + */ + public function children(): HasMany + { + return $this->hasMany(BuildingType::class, 'parent_id') + ->where('is_active', true); + } + + /** + * Retribution indices relationship + */ + public function indices(): HasOne + { + return $this->hasOne(RetributionIndex::class, 'building_type_id'); + } + + /** + * Calculations relationship + */ + public function calculations(): HasMany + { + return $this->hasMany(RetributionCalculation::class, 'building_type_id'); + } + + /** + * Scope: Active only + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + /** + * Scope: Parents only + */ + public function scopeParents($query) + { + return $query->whereNull('parent_id'); + } + + /** + * Scope: Children only + */ + public function scopeChildren($query) + { + return $query->whereNotNull('parent_id'); + } + + /** + * Scope: Non-free types + */ + public function scopeChargeable($query) + { + return $query->where('is_free', false); + } + + /** + * Check if building type is free + */ + public function isFree(): bool + { + return $this->is_free; + } + + /** + * Check if this is a parent type + */ + public function isParent(): bool + { + return $this->parent_id === null; + } + + /** + * Check if this is a child type + */ + public function isChild(): bool + { + return $this->parent_id !== null; + } + + /** + * Get complete data for calculation + */ + public function getCalculationData(): array + { + return [ + 'id' => $this->id, + 'code' => $this->code, + 'name' => $this->name, + 'coefficient' => $this->coefficient, + 'is_free' => $this->is_free, + 'indices' => $this->indices?->toArray(), + 'parent' => $this->parent?->only(['id', 'code', 'name']) + ]; + } +} \ No newline at end of file diff --git a/app/Models/HeightIndex.php b/app/Models/HeightIndex.php new file mode 100644 index 0000000..10d62af --- /dev/null +++ b/app/Models/HeightIndex.php @@ -0,0 +1,55 @@ + 'integer', + 'height_index' => 'decimal:6' + ]; + + /** + * Get height index by floor number + */ + public static function getByFloor(int $floorNumber): ?HeightIndex + { + return self::where('floor_number', $floorNumber)->first(); + } + + /** + * Get height index value by floor number + */ + public static function getHeightIndexByFloor(int $floorNumber): float + { + $index = self::getByFloor($floorNumber); + return $index ? (float) $index->height_index : 1.0; + } + + /** + * Get all height indices as array + */ + public static function getAllMapping(): array + { + return self::orderBy('floor_number') + ->pluck('height_index', 'floor_number') + ->toArray(); + } + + /** + * Get available floor numbers + */ + public static function getAvailableFloors(): array + { + return self::orderBy('floor_number') + ->pluck('floor_number') + ->toArray(); + } +} \ No newline at end of file diff --git a/app/Models/RetributionCalculation.php b/app/Models/RetributionCalculation.php new file mode 100644 index 0000000..5d38474 --- /dev/null +++ b/app/Models/RetributionCalculation.php @@ -0,0 +1,81 @@ + 'integer', + 'building_area' => 'decimal:2', + 'retribution_amount' => 'decimal:2', + 'calculation_detail' => 'array', + 'calculated_at' => 'datetime' + ]; + + /** + * Building type relationship + */ + public function buildingType(): BelongsTo + { + return $this->belongsTo(BuildingType::class, 'building_type_id'); + } + + /** + * Generate unique calculation ID + */ + public static function generateCalculationId(): string + { + return 'RTB' . Carbon::now()->format('ymdHis') . rand(10, 99); + } + + /** + * Create new calculation + */ + public static function createCalculation( + int $buildingTypeId, + int $floorNumber, + float $buildingArea, + float $retributionAmount, + array $calculationDetail + ): self { + return self::create([ + 'calculation_id' => self::generateCalculationId(), + 'building_type_id' => $buildingTypeId, + 'floor_number' => $floorNumber, + 'building_area' => $buildingArea, + 'retribution_amount' => $retributionAmount, + 'calculation_detail' => $calculationDetail, + 'calculated_at' => Carbon::now() + ]); + } + + /** + * Get formatted retribution amount + */ + public function getFormattedAmount(): string + { + return 'Rp ' . number_format($this->retribution_amount, 2, ',', '.'); + } + + /** + * Get calculation breakdown + */ + public function getCalculationBreakdown(): array + { + return $this->calculation_detail ?? []; + } +} \ No newline at end of file diff --git a/app/Models/RetributionConfig.php b/app/Models/RetributionConfig.php new file mode 100644 index 0000000..1700268 --- /dev/null +++ b/app/Models/RetributionConfig.php @@ -0,0 +1,50 @@ + 'decimal:2', + 'is_active' => 'boolean' + ]; + + /** + * Get config value by key + */ + public static function getValue(string $key, float $default = 0.0): float + { + $config = self::where('key', $key)->where('is_active', true)->first(); + return $config ? (float) $config->value : $default; + } + + /** + * Get all active configs as array + */ + public static function getAllActive(): array + { + return self::where('is_active', true) + ->pluck('value', 'key') + ->toArray(); + } + + /** + * Update config value + */ + public static function updateValue(string $key, float $value): bool + { + return self::updateOrCreate( + ['key' => $key], + ['value' => $value, 'is_active' => true] + ); + } +} \ No newline at end of file diff --git a/app/Models/RetributionIndex.php b/app/Models/RetributionIndex.php new file mode 100644 index 0000000..e9b8604 --- /dev/null +++ b/app/Models/RetributionIndex.php @@ -0,0 +1,57 @@ + 'decimal:4', + 'ip_permanent' => 'decimal:4', + 'ip_complexity' => 'decimal:4', + 'locality_index' => 'decimal:4', + 'infrastructure_factor' => 'decimal:4', + 'is_active' => 'boolean' + ]; + + /** + * Building type relationship + */ + public function buildingType(): BelongsTo + { + return $this->belongsTo(BuildingType::class, 'building_type_id'); + } + + /** + * Scope: Active only + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + /** + * Get all indices as array + */ + public function getIndicesArray(): array + { + return [ + 'ip_permanent' => $this->ip_permanent, + 'ip_complexity' => $this->ip_complexity, + 'locality_index' => $this->locality_index, + 'infrastructure_factor' => $this->infrastructure_factor + ]; + } +} \ No newline at end of file diff --git a/app/Services/RetributionCalculatorService.php b/app/Services/RetributionCalculatorService.php new file mode 100644 index 0000000..19a4093 --- /dev/null +++ b/app/Services/RetributionCalculatorService.php @@ -0,0 +1,243 @@ +findOrFail($buildingTypeId); + + // Check if building type is free + if ($buildingType->isFree()) { + return $this->createFreeResult($buildingType, $floorNumber, $buildingArea, $saveResult); + } + + // Get height index + $heightIndex = HeightIndex::getHeightIndexByFloor($floorNumber); + + // Get configuration values + $baseValue = RetributionConfig::getValue('BASE_VALUE', 70350); + $infrastructureMultiplier = RetributionConfig::getValue('INFRASTRUCTURE_MULTIPLIER', 0.5); + $heightMultiplier = RetributionConfig::getValue('HEIGHT_MULTIPLIER', 0.5); + + // Get indices + $indices = $buildingType->indices; + if (!$indices) { + throw new \Exception("Indices not found for building type: {$buildingType->name}"); + } + + // Calculate using Excel formula + $result = $this->executeCalculation( + $buildingType, + $indices, + $heightIndex, + $baseValue, + $infrastructureMultiplier, + $heightMultiplier, + $floorNumber, + $buildingArea + ); + + // Save result if requested + if ($saveResult) { + $calculation = RetributionCalculation::createCalculation( + $buildingTypeId, + $floorNumber, + $buildingArea, + $result['total_retribution'], + $result['calculation_detail'] + ); + $result['calculation_id'] = $calculation->calculation_id; + } + + return $result; + } + + /** + * Execute the main calculation logic + */ + protected function executeCalculation( + BuildingType $buildingType, + $indices, + float $heightIndex, + float $baseValue, + float $infrastructureMultiplier, + float $heightMultiplier, + int $floorNumber, + float $buildingArea + ): array { + // Step 1: Calculate H5 coefficient (Excel formula: RUNDOWN(($E5*($F5+$G5+(0.5*H$3))),4)) + // H5 = coefficient * (ip_permanent + ip_complexity + (height_multiplier * height_index)) + $h5Raw = $indices->coefficient * ( + $indices->ip_permanent + + $indices->ip_complexity + + ($heightMultiplier * $heightIndex) + ); + + // Apply RUNDOWN (floor to 4 decimal places) + $h5 = floor($h5Raw * 10000) / 10000; + + // Step 2: Main calculation (Excel: 1*D5*(N5*base_value*H5*1)) + // Main = building_area * locality_index * base_value * h5 + $mainCalculation = $buildingArea * $indices->locality_index * $baseValue * $h5; + + // Step 3: Infrastructure calculation (Excel: O3*(1*D5*(N5*base_value*H5*1))) + // Additional = infrastructure_multiplier * main_calculation + $infrastructureCalculation = $infrastructureMultiplier * $mainCalculation; + + // Step 4: Total retribution (Main + Infrastructure) + $totalRetribution = $mainCalculation + $infrastructureCalculation; + + return [ + 'building_type' => [ + 'id' => $buildingType->id, + 'code' => $buildingType->code, + 'name' => $buildingType->name, + 'is_free' => $buildingType->is_free + ], + 'input_parameters' => [ + 'building_area' => $buildingArea, + 'floor_number' => $floorNumber, + 'height_index' => $heightIndex, + 'base_value' => $baseValue, + 'infrastructure_multiplier' => $infrastructureMultiplier, + 'height_multiplier' => $heightMultiplier + ], + 'indices' => [ + 'coefficient' => $indices->coefficient, + 'ip_permanent' => $indices->ip_permanent, + 'ip_complexity' => $indices->ip_complexity, + 'locality_index' => $indices->locality_index, + 'infrastructure_factor' => $indices->infrastructure_factor + ], + 'calculation_steps' => [ + 'h5_coefficient' => [ + 'formula' => 'RUNDOWN((coefficient * (ip_permanent + ip_complexity + (height_multiplier * height_index))), 4)', + 'calculation' => "RUNDOWN(({$indices->coefficient} * ({$indices->ip_permanent} + {$indices->ip_complexity} + ({$heightMultiplier} * {$heightIndex}))), 4)", + 'raw_result' => $h5Raw, + 'result' => $h5 + ], + 'main_calculation' => [ + 'formula' => 'building_area * locality_index * base_value * h5', + 'calculation' => "{$buildingArea} * {$indices->locality_index} * {$baseValue} * {$h5}", + 'result' => $mainCalculation + ], + 'infrastructure_calculation' => [ + 'formula' => 'infrastructure_multiplier * main_calculation', + 'calculation' => "{$infrastructureMultiplier} * {$mainCalculation}", + 'result' => $infrastructureCalculation + ], + 'total_calculation' => [ + 'formula' => 'main_calculation + infrastructure_calculation', + 'calculation' => "{$mainCalculation} + {$infrastructureCalculation}", + 'result' => $totalRetribution + ] + ], + 'total_retribution' => $totalRetribution, + 'formatted_amount' => 'Rp ' . number_format($totalRetribution, 2, ',', '.'), + 'calculation_detail' => [ + 'h5_raw' => $h5Raw, + 'h5' => $h5, + 'main' => $mainCalculation, + 'infrastructure' => $infrastructureCalculation, + 'total' => $totalRetribution + ] + ]; + } + + /** + * Create result for free building types + */ + protected function createFreeResult( + BuildingType $buildingType, + int $floorNumber, + float $buildingArea, + bool $saveResult + ): array { + $result = [ + 'building_type' => [ + 'id' => $buildingType->id, + 'code' => $buildingType->code, + 'name' => $buildingType->name, + 'is_free' => true + ], + 'input_parameters' => [ + 'building_area' => $buildingArea, + 'floor_number' => $floorNumber + ], + 'total_retribution' => 0.0, + 'formatted_amount' => 'Rp 0 (Gratis)', + 'calculation_detail' => [ + 'reason' => 'Building type is free of charge', + 'total' => 0.0 + ] + ]; + + if ($saveResult) { + $calculation = RetributionCalculation::createCalculation( + $buildingType->id, + $floorNumber, + $buildingArea, + 0.0, + $result['calculation_detail'] + ); + $result['calculation_id'] = $calculation->calculation_id; + } + + return $result; + } + + /** + * Get calculation by ID + */ + public function getCalculationById(string $calculationId): ?RetributionCalculation + { + return RetributionCalculation::with('buildingType') + ->where('calculation_id', $calculationId) + ->first(); + } + + /** + * Get all available building types for calculation + */ + public function getAvailableBuildingTypes(): array + { + return BuildingType::with('indices') + ->active() + ->children() // Only child types can be used for calculation + ->get() + ->map(function ($type) { + return [ + 'id' => $type->id, + 'code' => $type->code, + 'name' => $type->name, + 'is_free' => $type->is_free, + 'has_indices' => $type->indices !== null, + 'coefficient' => $type->indices ? $type->indices->coefficient : null + ]; + }) + ->toArray(); + } + + /** + * Get all available floor numbers + */ + public function getAvailableFloors(): array + { + return HeightIndex::getAvailableFloors(); + } +} \ No newline at end of file diff --git a/database/migrations/2025_06_18_211300_create_new_retribution_structure.php b/database/migrations/2025_06_18_211300_create_new_retribution_structure.php new file mode 100644 index 0000000..ffcbb66 --- /dev/null +++ b/database/migrations/2025_06_18_211300_create_new_retribution_structure.php @@ -0,0 +1,95 @@ +id(); + $table->string('code', 10)->unique()->comment('Kode fungsi bangunan'); + $table->string('name', 100)->comment('Nama fungsi bangunan'); + $table->unsignedBigInteger('parent_id')->nullable()->comment('Parent ID untuk hierarki'); + $table->tinyInteger('level')->default(1)->comment('Level hierarki (1=parent, 2=child)'); + $table->boolean('is_free')->default(false)->comment('Apakah gratis (keagamaan, MBR)'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->index(['parent_id', 'level']); + $table->index('is_active'); + $table->foreign('parent_id')->references('id')->on('building_types')->onDelete('cascade'); + }); + + // 2. Tabel Parameter Indeks (Simplified) + Schema::create('retribution_indices', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('building_type_id'); + $table->decimal('coefficient', 8, 4)->comment('Koefisien fungsi bangunan'); + $table->decimal('ip_permanent', 8, 4)->comment('Indeks Permanensi'); + $table->decimal('ip_complexity', 8, 4)->comment('Indeks Kompleksitas'); + $table->decimal('locality_index', 8, 4)->comment('Indeks Lokalitas'); + $table->decimal('infrastructure_factor', 8, 4)->default(0.5)->comment('Faktor prasarana (default 50%)'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->unique('building_type_id'); + $table->foreign('building_type_id')->references('id')->on('building_types')->onDelete('cascade'); + }); + + // 3. Tabel Indeks Ketinggian (Simplified) + Schema::create('height_indices', function (Blueprint $table) { + $table->id(); + $table->tinyInteger('floor_number')->unique()->comment('Nomor lantai'); + $table->decimal('height_index', 8, 6)->comment('Indeks ketinggian'); + $table->timestamps(); + + $table->index('floor_number'); + }); + + // 4. Tabel Konfigurasi Global + Schema::create('retribution_configs', function (Blueprint $table) { + $table->id(); + $table->string('key', 50)->unique()->comment('Kunci konfigurasi'); + $table->decimal('value', 15, 2)->comment('Nilai konfigurasi'); + $table->string('description', 200)->comment('Deskripsi konfigurasi'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + + // 5. Tabel Hasil Perhitungan (Simplified) + Schema::create('retribution_calculations', function (Blueprint $table) { + $table->id(); + $table->string('calculation_id', 20)->unique()->comment('ID unik perhitungan'); + $table->unsignedBigInteger('building_type_id'); + $table->tinyInteger('floor_number'); + $table->decimal('building_area', 12, 2)->comment('Luas bangunan (m2)'); + $table->decimal('retribution_amount', 15, 2)->comment('Jumlah retribusi'); + $table->json('calculation_detail')->comment('Detail perhitungan'); + $table->timestamp('calculated_at'); + $table->timestamps(); + + $table->index(['building_type_id', 'floor_number']); + $table->index('calculated_at'); + $table->foreign('building_type_id')->references('id')->on('building_types'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('retribution_calculations'); + Schema::dropIfExists('retribution_configs'); + Schema::dropIfExists('height_indices'); + Schema::dropIfExists('retribution_indices'); + Schema::dropIfExists('building_types'); + } +}; \ No newline at end of file diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index ceff5fc..9bdc969 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -55,6 +55,7 @@ class DatabaseSeeder extends Seeder FloorHeightIndexSeeder::class, RetributionFormulaSeeder::class, RetributionProposalSeeder::class, + RetributionDataSeeder::class, // New optimized retribution data ]); } } diff --git a/database/seeders/RetributionDataSeeder.php b/database/seeders/RetributionDataSeeder.php new file mode 100644 index 0000000..c80b5b9 --- /dev/null +++ b/database/seeders/RetributionDataSeeder.php @@ -0,0 +1,71 @@ +insert([ + // Parent Functions + ['id' => 1, 'code' => 'KEAGAMAAN', 'name' => 'Fungsi Keagamaan', 'parent_id' => null, 'level' => 1, 'is_free' => true, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['id' => 2, 'code' => 'SOSBUDAYA', 'name' => 'Fungsi Sosial Budaya', 'parent_id' => null, 'level' => 1, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['id' => 3, 'code' => 'CAMPURAN', 'name' => 'Fungsi Campuran (lebih dari 1)', 'parent_id' => null, 'level' => 1, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['id' => 4, 'code' => 'USAHA', 'name' => 'Fungsi Usaha', 'parent_id' => null, 'level' => 1, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['id' => 5, 'code' => 'HUNIAN', 'name' => 'Fungsi Hunian', 'parent_id' => null, 'level' => 1, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + + // Child Functions + ['id' => 6, 'code' => 'CAMP_KECIL', 'name' => 'Campuran Kecil', 'parent_id' => 3, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['id' => 7, 'code' => 'CAMP_BESAR', 'name' => 'Campuran Besar', 'parent_id' => 3, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['id' => 8, 'code' => 'UMKM', 'name' => 'Fungsi Usaha (UMKM)', 'parent_id' => 4, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['id' => 9, 'code' => 'USH_BESAR', 'name' => 'Usaha Besar (Non-Mikro)', 'parent_id' => 4, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['id' => 10, 'code' => 'HUN_SEDH', 'name' => 'Hunian Sederhana <100', 'parent_id' => 5, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['id' => 11, 'code' => 'HUN_TSEDH', 'name' => 'Hunian Tidak Sederhana >100', 'parent_id' => 5, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['id' => 12, 'code' => 'MBR', 'name' => 'Rumah Tinggal MBR', 'parent_id' => 5, 'level' => 2, 'is_free' => true, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ]); + + // Seed Retribution Indices berdasarkan Excel (with coefficient moved here) + DB::table('retribution_indices')->insert([ + ['building_type_id' => 1, 'coefficient' => 0.0000, 'ip_permanent' => 0.4000, 'ip_complexity' => 0.0000, 'locality_index' => 0.0000, 'infrastructure_factor' => 0.5000, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], // Keagamaan + ['building_type_id' => 2, 'coefficient' => 0.3000, 'ip_permanent' => 0.4000, 'ip_complexity' => 0.6000, 'locality_index' => 0.0030, 'infrastructure_factor' => 0.5000, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], // Sosial Budaya + ['building_type_id' => 6, 'coefficient' => 0.6000, 'ip_permanent' => 0.4000, 'ip_complexity' => 0.6000, 'locality_index' => 0.0050, 'infrastructure_factor' => 0.5000, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], // Campuran Kecil + ['building_type_id' => 7, 'coefficient' => 0.8000, 'ip_permanent' => 0.4000, 'ip_complexity' => 0.6000, 'locality_index' => 0.0050, 'infrastructure_factor' => 0.5000, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], // Campuran Besar + ['building_type_id' => 8, 'coefficient' => 0.5000, 'ip_permanent' => 0.4000, 'ip_complexity' => 0.6000, 'locality_index' => 0.0040, 'infrastructure_factor' => 0.5000, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], // UMKM + ['building_type_id' => 9, 'coefficient' => 0.7000, 'ip_permanent' => 0.4000, 'ip_complexity' => 0.6000, 'locality_index' => 0.0050, 'infrastructure_factor' => 0.5000, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], // Usaha Besar + ['building_type_id' => 10, 'coefficient' => 0.1500, 'ip_permanent' => 0.4000, 'ip_complexity' => 0.3000, 'locality_index' => 0.0040, 'infrastructure_factor' => 0.5000, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], // Hunian Sederhana + ['building_type_id' => 11, 'coefficient' => 0.1700, 'ip_permanent' => 0.4000, 'ip_complexity' => 0.6000, 'locality_index' => 0.0040, 'infrastructure_factor' => 0.5000, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], // Hunian Tidak Sederhana + ['building_type_id' => 12, 'coefficient' => 0.0000, 'ip_permanent' => 0.4000, 'ip_complexity' => 0.0000, 'locality_index' => 0.0000, 'infrastructure_factor' => 0.5000, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], // MBR + ]); + + // Seed Height Indices berdasarkan Excel + DB::table('height_indices')->insert([ + ['floor_number' => 1, 'height_index' => 1.0000, 'created_at' => now(), 'updated_at' => now()], + ['floor_number' => 2, 'height_index' => 1.0900, 'created_at' => now(), 'updated_at' => now()], + ['floor_number' => 3, 'height_index' => 1.1200, 'created_at' => now(), 'updated_at' => now()], + ['floor_number' => 4, 'height_index' => 1.1350, 'created_at' => now(), 'updated_at' => now()], + ['floor_number' => 5, 'height_index' => 1.1620, 'created_at' => now(), 'updated_at' => now()], + ['floor_number' => 6, 'height_index' => 1.1970, 'created_at' => now(), 'updated_at' => now()], + ]); + + // Seed Retribution Configs + DB::table('retribution_configs')->insert([ + ['key' => 'BASE_VALUE', 'value' => 7035000.00, 'description' => 'Nilai dasar perhitungan retribusi', 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['key' => 'INFRASTRUCTURE_MULTIPLIER', 'value' => 0.50, 'description' => 'Pengali asumsi prasarana (50%)', 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['key' => 'HEIGHT_MULTIPLIER', 'value' => 0.50, 'description' => 'Pengali indeks ketinggian dalam formula', 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ]); + + $this->command->info('✅ Retribution data seeded successfully!'); + $this->command->info('📊 Building Types: 12 records'); + $this->command->info('📊 Retribution Indices: 9 records'); + $this->command->info('📊 Height Indices: 6 records'); + $this->command->info('📊 Retribution Configs: 3 records'); + } +}