restructure retribution calculations table
This commit is contained in:
210
OPTIMIZED_TABLE_STRUCTURE.md
Normal file
210
OPTIMIZED_TABLE_STRUCTURE.md
Normal file
@@ -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**!
|
||||||
263
app/Console/Commands/TestRetributionCalculation.php
Normal file
263
app/Console/Commands/TestRetributionCalculation.php
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Services\RetributionCalculatorService;
|
||||||
|
use App\Models\BuildingType;
|
||||||
|
|
||||||
|
class TestRetributionCalculation extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'retribution:test
|
||||||
|
{--area= : Luas bangunan dalam m2}
|
||||||
|
{--floor= : Jumlah lantai (1-6)}
|
||||||
|
{--type= : ID atau kode building type}
|
||||||
|
{--all : Test semua building types}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Test perhitungan retribusi PBG dengan input luas bangunan dan tinggi lantai';
|
||||||
|
|
||||||
|
protected $calculatorService;
|
||||||
|
|
||||||
|
public function __construct(RetributionCalculatorService $calculatorService)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->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('🏢 <fg=cyan>Building Type:</> ' . $result['building_type']['name']);
|
||||||
|
$this->line('📐 <fg=cyan>Luas Bangunan:</> ' . number_format($area, 0) . ' m²');
|
||||||
|
$this->line('🏗️ <fg=cyan>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('💰 <fg=green>Total Retribusi: Rp 0</fg=green>');
|
||||||
|
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('💰 <fg=green>TOTAL RETRIBUSI: ' . $result['formatted_amount'] . '</fg=green>');
|
||||||
|
$this->line('📈 <fg=yellow>Per m²: Rp ' . number_format($result['total_retribution'] / $area, 2) . '</fg=yellow>');
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
291
app/Console/Commands/TestRetributionData.php
Normal file
291
app/Console/Commands/TestRetributionData.php
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Models\BuildingType;
|
||||||
|
use App\Models\RetributionIndex;
|
||||||
|
use App\Models\HeightIndex;
|
||||||
|
use App\Models\RetributionConfig;
|
||||||
|
use App\Models\RetributionCalculation;
|
||||||
|
use App\Services\RetributionCalculatorService;
|
||||||
|
|
||||||
|
class TestRetributionData extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'retribution:data
|
||||||
|
{--save : Save calculation results to database}
|
||||||
|
{--show : Show existing data and relations}
|
||||||
|
{--clear : Clear calculation history}';
|
||||||
|
|
||||||
|
protected $description = 'Test retribution data storage and display database relations';
|
||||||
|
|
||||||
|
protected $calculatorService;
|
||||||
|
|
||||||
|
public function __construct(RetributionCalculatorService $calculatorService)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->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("📋 <info>{$table}</info>: {$description}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->line('');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function showSampleData()
|
||||||
|
{
|
||||||
|
$this->info('📋 SAMPLE DATA DARI SETIAP TABEL');
|
||||||
|
$this->info('=' . str_repeat('=', 35));
|
||||||
|
|
||||||
|
// Building Types
|
||||||
|
$this->line('<comment>🏢 BUILDING TYPES:</comment>');
|
||||||
|
$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('<comment>📊 RETRIBUTION INDICES:</comment>');
|
||||||
|
$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('<comment>🏗️ HEIGHT INDICES:</comment>');
|
||||||
|
$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('<comment>⚙️ RETRIBUTION CONFIGS:</comment>');
|
||||||
|
$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("<comment>🏢 Building Type: {$buildingType->name}</comment>");
|
||||||
|
$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(" <comment>📊 Retribution Index:</comment>");
|
||||||
|
$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(" <comment>📈 Calculations: {$calculationsCount} records</comment>");
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
131
app/Models/BuildingType.php
Normal file
131
app/Models/BuildingType.php
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
|
||||||
|
class BuildingType extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
'parent_id',
|
||||||
|
'level',
|
||||||
|
'is_free',
|
||||||
|
'is_active'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'level' => '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'])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
55
app/Models/HeightIndex.php
Normal file
55
app/Models/HeightIndex.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class HeightIndex extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'floor_number',
|
||||||
|
'height_index'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'floor_number' => '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();
|
||||||
|
}
|
||||||
|
}
|
||||||
81
app/Models/RetributionCalculation.php
Normal file
81
app/Models/RetributionCalculation.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class RetributionCalculation extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'calculation_id',
|
||||||
|
'building_type_id',
|
||||||
|
'floor_number',
|
||||||
|
'building_area',
|
||||||
|
'retribution_amount',
|
||||||
|
'calculation_detail',
|
||||||
|
'calculated_at'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'floor_number' => '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 ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
50
app/Models/RetributionConfig.php
Normal file
50
app/Models/RetributionConfig.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class RetributionConfig extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'key',
|
||||||
|
'value',
|
||||||
|
'description',
|
||||||
|
'is_active'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'value' => '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]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
app/Models/RetributionIndex.php
Normal file
57
app/Models/RetributionIndex.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class RetributionIndex extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'building_type_id',
|
||||||
|
'coefficient',
|
||||||
|
'ip_permanent',
|
||||||
|
'ip_complexity',
|
||||||
|
'locality_index',
|
||||||
|
'infrastructure_factor',
|
||||||
|
'is_active'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'coefficient' => '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
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
243
app/Services/RetributionCalculatorService.php
Normal file
243
app/Services/RetributionCalculatorService.php
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\BuildingType;
|
||||||
|
use App\Models\HeightIndex;
|
||||||
|
use App\Models\RetributionConfig;
|
||||||
|
use App\Models\RetributionCalculation;
|
||||||
|
|
||||||
|
class RetributionCalculatorService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Calculate retribution for given parameters
|
||||||
|
*/
|
||||||
|
public function calculate(
|
||||||
|
int $buildingTypeId,
|
||||||
|
int $floorNumber,
|
||||||
|
float $buildingArea,
|
||||||
|
bool $saveResult = true
|
||||||
|
): array {
|
||||||
|
// Get building type with indices
|
||||||
|
$buildingType = BuildingType::with('indices')->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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// 1. Tabel Fungsi Bangunan (Simplified)
|
||||||
|
Schema::create('building_types', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -55,6 +55,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
FloorHeightIndexSeeder::class,
|
FloorHeightIndexSeeder::class,
|
||||||
RetributionFormulaSeeder::class,
|
RetributionFormulaSeeder::class,
|
||||||
RetributionProposalSeeder::class,
|
RetributionProposalSeeder::class,
|
||||||
|
RetributionDataSeeder::class, // New optimized retribution data
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
71
database/seeders/RetributionDataSeeder.php
Normal file
71
database/seeders/RetributionDataSeeder.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class RetributionDataSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Seed Building Types berdasarkan Excel (without coefficient)
|
||||||
|
DB::table('building_types')->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user