findOrFail($buildingFunctionId); $parameters = $buildingFunction->parameter; if (!$parameters) { throw new \Exception("Building function parameters not found for ID: {$buildingFunctionId}"); } // Get floor height index $floorHeightIndex = FloorHeightIndex::where('floor_number', $floorNumber)->first(); if (!$floorHeightIndex) { throw new \Exception("Floor height index not found for floor: {$floorNumber}"); } // Get retribution formula $retributionFormula = RetributionFormula::where('building_function_id', $buildingFunctionId) ->where('floor_number', $floorNumber) ->first(); if (!$retributionFormula) { throw new \Exception("Retribution formula not found for building function ID: {$buildingFunctionId}, floor: {$floorNumber}"); } // Calculate retribution using Excel formula $calculationResult = $this->calculateRetribution( $floorArea, $parameters, $floorHeightIndex->ip_ketinggian ); // Create retribution proposal return RetributionProposal::create([ 'spatial_planning_id' => $spatialPlanning->id, 'building_function_id' => $buildingFunctionId, 'retribution_formula_id' => $retributionFormula->id, 'floor_number' => $floorNumber, 'floor_area' => $floorArea, 'total_building_area' => $totalBuildingArea ?? $floorArea, 'ip_ketinggian' => $floorHeightIndex->ip_ketinggian, 'floor_retribution_amount' => $calculationResult['total_retribution'], 'total_retribution_amount' => $calculationResult['total_retribution'], 'calculation_parameters' => $calculationResult['parameters'], 'calculation_breakdown' => $calculationResult['breakdown'], 'notes' => $notes, 'calculated_at' => Carbon::now() ]); } /** * Create proposal without spatial planning */ public function createStandaloneProposal( int $buildingFunctionId, int $floorNumber, float $floorArea, float $totalBuildingArea = null, string $notes = null ): RetributionProposal { // Get building function and its parameters $buildingFunction = BuildingFunction::with('parameter')->findOrFail($buildingFunctionId); $parameters = $buildingFunction->parameter; if (!$parameters) { throw new \Exception("Building function parameters not found for ID: {$buildingFunctionId}"); } // Get floor height index $floorHeightIndex = FloorHeightIndex::where('floor_number', $floorNumber)->first(); if (!$floorHeightIndex) { throw new \Exception("Floor height index not found for floor: {$floorNumber}"); } // Get retribution formula $retributionFormula = RetributionFormula::where('building_function_id', $buildingFunctionId) ->where('floor_number', $floorNumber) ->first(); if (!$retributionFormula) { throw new \Exception("Retribution formula not found for building function ID: {$buildingFunctionId}, floor: {$floorNumber}"); } // Calculate retribution using Excel formula $calculationResult = $this->calculateRetribution( $floorArea, $parameters, $floorHeightIndex->ip_ketinggian ); // Create retribution proposal return RetributionProposal::create([ 'spatial_planning_id' => null, 'building_function_id' => $buildingFunctionId, 'retribution_formula_id' => $retributionFormula->id, 'floor_number' => $floorNumber, 'floor_area' => $floorArea, 'total_building_area' => $totalBuildingArea ?? $floorArea, 'ip_ketinggian' => $floorHeightIndex->ip_ketinggian, 'floor_retribution_amount' => $calculationResult['total_retribution'], 'total_retribution_amount' => $calculationResult['total_retribution'], 'calculation_parameters' => $calculationResult['parameters'], 'calculation_breakdown' => $calculationResult['breakdown'], 'notes' => $notes, 'calculated_at' => Carbon::now() ]); } /** * Calculate retribution using Excel formula */ protected function calculateRetribution( float $floorArea, BuildingFunctionParameter $parameters, float $ipKetinggian ): array { // Excel formula parameters $fungsi_bangunan = $parameters->fungsi_bangunan; $ip_permanen = $parameters->ip_permanen; $ip_kompleksitas = $parameters->ip_kompleksitas; $indeks_lokalitas = $parameters->indeks_lokalitas; $base_value = 70350; $additional_factor = 0.5; // Step 1: Calculate H13 (floor coefficient) $h13 = $fungsi_bangunan * ($ip_permanen + $ip_kompleksitas + (0.5 * $ipKetinggian)); // Step 2: Main calculation $main_calculation = 1 * $floorArea * ($indeks_lokalitas * $base_value * $h13 * 1); // Step 3: Additional (50%) $additional_calculation = $additional_factor * $main_calculation; // Step 4: Total $total_retribution = $main_calculation + $additional_calculation; return [ 'total_retribution' => $total_retribution, 'parameters' => [ 'fungsi_bangunan' => $fungsi_bangunan, 'ip_permanen' => $ip_permanen, 'ip_kompleksitas' => $ip_kompleksitas, 'ip_ketinggian' => $ipKetinggian, 'indeks_lokalitas' => $indeks_lokalitas, 'base_value' => $base_value, 'additional_factor' => $additional_factor, 'floor_area' => $floorArea ], 'breakdown' => [ 'h13_calculation' => [ 'formula' => 'fungsi_bangunan * (ip_permanen + ip_kompleksitas + (0.5 * ip_ketinggian))', 'calculation' => "{$fungsi_bangunan} * ({$ip_permanen} + {$ip_kompleksitas} + (0.5 * {$ipKetinggian}))", 'result' => $h13 ], 'main_calculation' => [ 'formula' => '1 * floor_area * (indeks_lokalitas * base_value * h13 * 1)', 'calculation' => "1 * {$floorArea} * ({$indeks_lokalitas} * {$base_value} * {$h13} * 1)", 'result' => $main_calculation ], 'additional_calculation' => [ 'formula' => 'additional_factor * main_calculation', 'calculation' => "{$additional_factor} * {$main_calculation}", 'result' => $additional_calculation ], 'total_calculation' => [ 'formula' => 'main_calculation + additional_calculation', 'calculation' => "{$main_calculation} + {$additional_calculation}", 'result' => $total_retribution ] ] ]; } /** * Get proposal statistics */ public function getStatistics(): array { return [ 'total_proposals' => RetributionProposal::count(), 'total_amount' => RetributionProposal::sum('total_retribution_amount'), 'average_amount' => RetributionProposal::avg('total_retribution_amount'), 'proposals_with_spatial_planning' => RetributionProposal::whereNotNull('spatial_planning_id')->count(), 'proposals_without_spatial_planning' => RetributionProposal::whereNull('spatial_planning_id')->count(), 'by_building_function' => RetributionProposal::with('buildingFunction') ->selectRaw('building_function_id, COUNT(*) as count, SUM(total_retribution_amount) as total_amount') ->groupBy('building_function_id') ->orderBy('total_amount', 'desc') ->get() ->map(function ($item) { return [ 'building_function_id' => $item->building_function_id, 'building_function_name' => $item->buildingFunction->name ?? 'Unknown', 'count' => $item->count, 'total_amount' => $item->total_amount, 'formatted_amount' => 'Rp ' . number_format($item->total_amount, 0, ',', '.') ]; })->toArray() ]; } /** * Detect building function from text */ public function detectBuildingFunction(string $text): ?BuildingFunction { $text = strtolower($text); // Detection patterns - order matters (more specific first) $patterns = [ 'HUNIAN_TIDAK_SEDERHANA' => [ 'hunian mewah', 'villa', 'apartemen', 'kondominium', 'townhouse' ], 'HUNIAN_SEDERHANA' => [ 'hunian sederhana', 'hunian', 'rumah', 'perumahan', 'residential', 'fungsi hunian' ], 'USAHA_BESAR' => [ 'usaha besar', 'pabrik', 'industri', 'mall', 'hotel', 'restoran', 'supermarket', 'plaza', 'gedung perkantoran', 'non-mikro', 'non mikro' ], 'USAHA_KECIL' => [ 'usaha kecil', 'umkm', 'warung', 'toko', 'mikro', 'kios' ], 'CAMPURAN_BESAR' => [ 'campuran besar', 'mixed use besar' ], 'CAMPURAN_KECIL' => [ 'campuran kecil', 'ruko', 'mixed use', 'campuran' ], 'SOSIAL_BUDAYA' => [ 'sekolah', 'rumah sakit', 'masjid', 'gereja', 'puskesmas', 'klinik', 'universitas', 'perpustakaan', 'museum' ], 'AGAMA' => [ 'tempat ibadah', 'masjid', 'mushola', 'gereja', 'pura', 'vihara', 'klenteng' ] ]; // Try to find exact matches first foreach ($patterns as $functionCode => $keywords) { foreach ($keywords as $keyword) { if (strpos($text, $keyword) !== false) { $buildingFunction = BuildingFunction::where('code', $functionCode)->first(); if ($buildingFunction) { return $buildingFunction; } } } } // Debug: Log what we're trying to match \Illuminate\Support\Facades\Log::info("Building function detection failed for text: '{$text}'"); // If no exact match, try to find by name similarity $allFunctions = BuildingFunction::whereNotNull('parent_id')->get(); // Only child functions foreach ($allFunctions as $function) { $functionName = strtolower($function->name); // Check if any word from the function name appears in the text $words = explode(' ', $functionName); foreach ($words as $word) { if (strlen($word) > 3 && strpos($text, $word) !== false) { return $function; } } } return null; } }