diff --git a/app/Console/Commands/AssignSpatialPlanningsToCalculation.php b/app/Console/Commands/AssignSpatialPlanningsToCalculation.php new file mode 100644 index 0000000..e066d1b --- /dev/null +++ b/app/Console/Commands/AssignSpatialPlanningsToCalculation.php @@ -0,0 +1,467 @@ +calculatorService = $calculatorService; + } + + /** + * Execute the console command. + */ + public function handle() + { + $this->info('๐Ÿ—๏ธ Starting spatial planning calculation assignment...'); + + // Get processing options + $force = $this->option('force'); + $recalculate = $this->option('recalculate'); + $chunkSize = (int) $this->option('chunk'); + + // Get spatial plannings query + $query = SpatialPlanning::query(); + + if ($recalculate) { + // Recalculate mode: only process those WITH active calculations + $query->whereHas('retributionCalculations', function ($q) { + $q->where('is_active', true); + }); + $this->info('๐Ÿ”„ Recalculate mode: Processing spatial plannings with existing calculations'); + } elseif (!$force) { + // Normal mode: only process those without active calculations + $query->whereDoesntHave('retributionCalculations', function ($q) { + $q->where('is_active', true); + }); + $this->info('โž• Normal mode: Processing spatial plannings without calculations'); + } else { + // Force mode: process all + $this->info('๐Ÿ”ฅ Force mode: Processing ALL spatial plannings'); + } + + $totalRecords = $query->count(); + + if ($totalRecords === 0) { + $this->warn('No spatial plannings found to process.'); + return 0; + } + + $this->info("Found {$totalRecords} spatial planning(s) to process"); + + if (!$this->confirm('Do you want to continue?')) { + $this->info('Operation cancelled.'); + return 0; + } + + // Process in chunks + $processed = 0; + $errors = 0; + $reused = 0; + $created = 0; + $buildingTypeStats = []; + + $progressBar = $this->output->createProgressBar($totalRecords); + $progressBar->start(); + + $recalculated = 0; + + $query->chunk($chunkSize, function ($spatialPlannings) use (&$processed, &$errors, &$reused, &$created, &$recalculated, &$buildingTypeStats, $progressBar, $recalculate) { + foreach ($spatialPlannings as $spatialPlanning) { + try { + $result = $this->assignCalculationToSpatialPlanning($spatialPlanning, $recalculate); + + if ($result['reused']) { + $reused++; + } elseif (isset($result['recalculated']) && $result['recalculated']) { + $recalculated++; + } else { + $created++; + } + + // Track building type statistics + $buildingTypeName = $result['building_type_name'] ?? 'Unknown'; + if (!isset($buildingTypeStats[$buildingTypeName])) { + $buildingTypeStats[$buildingTypeName] = 0; + } + $buildingTypeStats[$buildingTypeName]++; + + $processed++; + } catch (\Exception $e) { + $errors++; + $this->error("Error processing ID {$spatialPlanning->id}: " . $e->getMessage()); + } + + $progressBar->advance(); + } + }); + + $progressBar->finish(); + + // Show summary + $this->newLine(2); + $this->info('โœ… Assignment completed!'); + + if ($recalculate) { + $this->table( + ['Metric', 'Count'], + [ + ['Total Processed', $processed], + ['Recalculated (Changed)', $recalculated], + ['Unchanged', $reused], + ['Errors', $errors], + ] + ); + } else { + $this->table( + ['Metric', 'Count'], + [ + ['Total Processed', $processed], + ['Calculations Created', $created], + ['Calculations Reused', $reused], + ['Errors', $errors], + ] + ); + } + + // Show building type statistics + if (!empty($buildingTypeStats)) { + $this->newLine(); + $this->info('๐Ÿ“Š Building Type Distribution:'); + $statsRows = []; + arsort($buildingTypeStats); // Sort by count descending + foreach ($buildingTypeStats as $typeName => $count) { + $percentage = round(($count / $processed) * 100, 1); + $statsRows[] = [$typeName, $count, $percentage . '%']; + } + $this->table(['Building Type', 'Count', 'Percentage'], $statsRows); + } + + return 0; + } + + /** + * Assign calculation to a spatial planning + */ + private function assignCalculationToSpatialPlanning(SpatialPlanning $spatialPlanning, bool $recalculate = false): array + { + // 1. Detect building type + $buildingType = $this->detectBuildingType($spatialPlanning->building_function); + + // 2. Get calculation parameters (round to 2 decimal places) + $floorNumber = $spatialPlanning->number_of_floors ?: 1; + $buildingArea = round($spatialPlanning->getCalculationArea(), 2); + + if ($buildingArea <= 0) { + throw new \Exception("Invalid building area: {$buildingArea}"); + } + + $reused = false; + $isRecalculated = false; + + if ($recalculate) { + // Recalculate mode: Always create new calculation + $calculationResult = $this->performCalculation($spatialPlanning, $buildingType); + + // Check if spatial planning has existing active calculation + $currentActiveCalculation = $spatialPlanning->activeRetributionCalculation; + + if ($currentActiveCalculation) { + $oldAmount = $currentActiveCalculation->retributionCalculation->retribution_amount; + $oldArea = $currentActiveCalculation->retributionCalculation->building_area; + $newAmount = $calculationResult['amount']; + + // Check if there's a significant difference (more than 1 rupiah) + if (abs($oldAmount - $newAmount) > 1) { + // Create new calculation + $calculation = RetributionCalculation::create([ + 'building_type_id' => $buildingType->id, + 'floor_number' => $floorNumber, + 'building_area' => $buildingArea, + 'retribution_amount' => $calculationResult['amount'], + 'calculation_detail' => $calculationResult['detail'], + ]); + + // Assign new calculation + $spatialPlanning->assignRetributionCalculation( + $calculation, + "Recalculated: Area {$oldArea}โ†’{$buildingArea}, Amount {$oldAmount}โ†’{$newAmount}" + ); + + $isRecalculated = true; + } else { + // No significant difference, keep existing + $calculation = $currentActiveCalculation->retributionCalculation; + $reused = true; + } + } else { + // No existing calculation, create new + $calculation = RetributionCalculation::create([ + 'building_type_id' => $buildingType->id, + 'floor_number' => $floorNumber, + 'building_area' => $buildingArea, + 'retribution_amount' => $calculationResult['amount'], + 'calculation_detail' => $calculationResult['detail'], + ]); + + $spatialPlanning->assignRetributionCalculation( + $calculation, + 'Recalculated (new calculation)' + ); + } + } else { + // Normal mode: Check if calculation already exists with same parameters + $existingCalculation = RetributionCalculation::where([ + 'building_type_id' => $buildingType->id, + 'floor_number' => $floorNumber, + ]) + ->whereBetween('building_area', [ + $buildingArea * 0.99, // 1% tolerance + $buildingArea * 1.01 + ]) + ->first(); + + if ($existingCalculation) { + // Reuse existing calculation + $calculation = $existingCalculation; + $reused = true; + } else { + // Create new calculation + $calculationResult = $this->performCalculation($spatialPlanning, $buildingType); + + $calculation = RetributionCalculation::create([ + 'building_type_id' => $buildingType->id, + 'floor_number' => $floorNumber, + 'building_area' => $buildingArea, + 'retribution_amount' => $calculationResult['amount'], + 'calculation_detail' => $calculationResult['detail'], + ]); + } + + // Assign to spatial planning + $spatialPlanning->assignRetributionCalculation( + $calculation, + $reused ? 'Auto-assigned (reused calculation)' : 'Auto-assigned (new calculation)' + ); + } + + return [ + 'calculation' => $calculation, + 'reused' => $reused, + 'recalculated' => $isRecalculated, + 'building_type_name' => $buildingType->name, + 'building_type_code' => $buildingType->code, + ]; + } + + /** + * Detect building type based on building function using database + */ + private function detectBuildingType(string $buildingFunction = null): BuildingType + { + $function = strtolower($buildingFunction ?? ''); + + // Mapping building functions to building type codes from database + $mappings = [ + // Religious + 'masjid' => 'KEAGAMAAN', + 'gereja' => 'KEAGAMAAN', + 'vihara' => 'KEAGAMAAN', + 'pura' => 'KEAGAMAAN', + 'keagamaan' => 'KEAGAMAAN', + 'religious' => 'KEAGAMAAN', + + // Residential/Housing + 'rumah' => 'HUN_SEDH', // Default to simple housing + 'perumahan' => 'HUN_SEDH', + 'hunian' => 'HUN_SEDH', + 'residential' => 'HUN_SEDH', + 'tinggal' => 'HUN_SEDH', + 'mbr' => 'MBR', // Specifically for MBR + 'masyarakat berpenghasilan rendah' => 'MBR', + + // Commercial/Business - default to UMKM + 'toko' => 'UMKM', + 'warung' => 'UMKM', + 'perdagangan' => 'UMKM', + 'dagang' => 'UMKM', + 'usaha' => 'UMKM', + 'komersial' => 'UMKM', + 'commercial' => 'UMKM', + 'pasar' => 'UMKM', + 'kios' => 'UMKM', + + // Large commercial + 'mall' => 'USH_BESAR', + 'plaza' => 'USH_BESAR', + 'supermarket' => 'USH_BESAR', + 'department' => 'USH_BESAR', + 'hotel' => 'USH_BESAR', + 'resort' => 'USH_BESAR', + + // Office + 'kantor' => 'UMKM', // Can be UMKM or USH_BESAR depending on size + 'perkantoran' => 'UMKM', + 'office' => 'UMKM', + + // Industry (usually big business) + 'industri' => 'USH_BESAR', + 'pabrik' => 'USH_BESAR', + 'gudang' => 'USH_BESAR', + 'warehouse' => 'USH_BESAR', + 'manufacturing' => 'USH_BESAR', + + // Social/Cultural + 'sekolah' => 'SOSBUDAYA', + 'pendidikan' => 'SOSBUDAYA', + 'universitas' => 'SOSBUDAYA', + 'kampus' => 'SOSBUDAYA', + 'rumah sakit' => 'SOSBUDAYA', + 'klinik' => 'SOSBUDAYA', + 'kesehatan' => 'SOSBUDAYA', + 'puskesmas' => 'SOSBUDAYA', + 'museum' => 'SOSBUDAYA', + 'perpustakaan' => 'SOSBUDAYA', + 'gedung olahraga' => 'SOSBUDAYA', + + // Mixed use + 'campuran' => 'CAMP_KECIL', // Default to small mixed + 'mixed' => 'CAMP_KECIL', + ]; + + // Try to match building function + $detectedCode = null; + foreach ($mappings as $keyword => $code) { + if (str_contains($function, $keyword)) { + $detectedCode = $code; + break; + } + } + + // Find building type in database by code + if ($detectedCode) { + $buildingType = BuildingType::where('code', $detectedCode) + ->whereHas('indices') // Only types with indices + ->first(); + + if ($buildingType) { + return $buildingType; + } + } + + // Default to "UMKM" type if not detected (most common business type) + $defaultType = BuildingType::where('code', 'UMKM') + ->whereHas('indices') + ->first(); + + if ($defaultType) { + return $defaultType; + } + + // Fallback to any available type with indices + $fallbackType = BuildingType::whereHas('indices') + ->where('is_active', true) + ->first(); + + if (!$fallbackType) { + throw new \Exception('No building types with indices found in database. Please run: php artisan db:seed --class=RetributionDataSeeder'); + } + + return $fallbackType; + } + + /** + * Perform calculation using RetributionCalculatorService + */ + private function performCalculation(SpatialPlanning $spatialPlanning, BuildingType $buildingType): array + { + // Round area to 2 decimal places to match database storage format + $buildingArea = round($spatialPlanning->getCalculationArea(), 2); + $floorNumber = $spatialPlanning->number_of_floors ?: 1; + + try { + // Use the same calculation service as TestRetributionCalculation + $result = $this->calculatorService->calculate( + $buildingType->id, + $floorNumber, + $buildingArea, + false // Don't save to database, we'll handle that separately + ); + + return [ + 'amount' => $result['total_retribution'], + 'detail' => [ + 'building_type_id' => $buildingType->id, + 'building_type_name' => $buildingType->name, + 'building_type_code' => $buildingType->code, + 'coefficient' => $result['indices']['coefficient'], + 'ip_permanent' => $result['indices']['ip_permanent'], + 'ip_complexity' => $result['indices']['ip_complexity'], + 'locality_index' => $result['indices']['locality_index'], + 'height_index' => $result['input_parameters']['height_index'], + 'infrastructure_factor' => $result['indices']['infrastructure_factor'], + 'building_area' => $buildingArea, + 'floor_number' => $floorNumber, + 'building_function' => $spatialPlanning->building_function, + 'calculation_steps' => $result['calculation_detail'], + 'base_value' => $result['input_parameters']['base_value'], + 'is_free' => $buildingType->is_free, + 'calculation_date' => now()->toDateTimeString(), + 'total' => $result['total_retribution'], + ] + ]; + + } catch (\Exception $e) { + // Fallback to basic calculation if service fails + $this->warn("Calculation service failed for {$spatialPlanning->name}: {$e->getMessage()}. Using fallback calculation."); + + // Basic fallback calculation + $totalAmount = $buildingType->is_free ? 0 : ($buildingArea * 50000); + + return [ + 'amount' => $totalAmount, + 'detail' => [ + 'building_type_id' => $buildingType->id, + 'building_type_name' => $buildingType->name, + 'building_type_code' => $buildingType->code, + 'building_area' => $buildingArea, + 'floor_number' => $floorNumber, + 'building_function' => $spatialPlanning->building_function, + 'calculation_method' => 'fallback', + 'error_message' => $e->getMessage(), + 'is_free' => $buildingType->is_free, + 'calculation_date' => now()->toDateTimeString(), + 'total' => $totalAmount, + ] + ]; + } + } +} diff --git a/app/Console/Commands/CalculateRetributionProposalsCommand.php b/app/Console/Commands/CalculateRetributionProposalsCommand.php deleted file mode 100644 index 63fd5a2..0000000 --- a/app/Console/Commands/CalculateRetributionProposalsCommand.php +++ /dev/null @@ -1,288 +0,0 @@ -proposalService = $proposalService; - } - - /** - * Execute the console command. - */ - public function handle() - { - $this->info('๐Ÿงฎ Starting Retribution Proposal Calculation...'); - $this->newLine(); - - try { - // Get processing options - $force = $this->option('force'); - $limit = $this->option('limit') ? (int) $this->option('limit') : null; - $skipExisting = $this->option('skip-existing'); - $dryRun = $this->option('dry-run'); - - // Build query for spatial plannings - $query = SpatialPlanning::query(); - - if ($skipExisting && !$force) { - $query->whereDoesntHave('retributionProposals'); - } - - if ($limit) { - $query->limit($limit); - } - - $spatialPlannings = $query->get(); - $totalCount = $spatialPlannings->count(); - - if ($totalCount === 0) { - $this->warn('No spatial plannings found to process.'); - return 0; - } - - // Show processing summary - $this->info("๐Ÿ“Š PROCESSING SUMMARY:"); - $this->info(" Total Spatial Plannings: {$totalCount}"); - $this->info(" Force Recalculate: " . ($force ? 'Yes' : 'No')); - $this->info(" Skip Existing: " . ($skipExisting ? 'Yes' : 'No')); - $this->info(" Dry Run: " . ($dryRun ? 'Yes' : 'No')); - $this->newLine(); - - if ($dryRun) { - $this->showDryRunPreview($spatialPlannings); - return 0; - } - - // Confirm processing - if (!$this->confirm("Process {$totalCount} spatial plannings?")) { - $this->info('Operation cancelled.'); - return 0; - } - - // Process spatial plannings - $this->processRetributionCalculations($spatialPlannings, $force); - - return 0; - - } catch (Exception $e) { - $this->error('Error during retribution calculation: ' . $e->getMessage()); - return 1; - } - } - - /** - * Show dry run preview - */ - private function showDryRunPreview($spatialPlannings) - { - $this->info('๐Ÿ” DRY RUN PREVIEW:'); - $this->newLine(); - - $processable = 0; - $withProposals = 0; - $withoutFunction = 0; - $withoutArea = 0; - - foreach ($spatialPlannings as $spatial) { - $hasProposals = $spatial->retributionProposals()->exists(); - $buildingFunction = $this->proposalService->detectBuildingFunction($spatial->getBuildingFunctionText()); - $area = $spatial->getCalculationArea(); - - if ($hasProposals) { - $withProposals++; - } - - if (!$buildingFunction) { - $withoutFunction++; - } - - if ($area <= 0) { - $withoutArea++; - } - - if ($buildingFunction && $area > 0) { - $processable++; - } - } - - $this->table( - ['Status', 'Count', 'Description'], - [ - ['โœ… Processable', $processable, 'Can create retribution proposals'], - ['๐Ÿ”„ With Existing Proposals', $withProposals, 'Already have proposals (will skip unless --force)'], - ['โŒ Missing Building Function', $withoutFunction, 'Cannot detect building function'], - ['โŒ Missing Area', $withoutArea, 'Area is zero or missing'], - ] - ); - - $this->newLine(); - $this->info("๐Ÿ’ก To process: php artisan retribution:calculate-proposals"); - $this->info("๐Ÿ’ก To force recalculate: php artisan retribution:calculate-proposals --force"); - } - - /** - * Process retribution calculations - */ - private function processRetributionCalculations($spatialPlannings, $force) - { - $progressBar = $this->output->createProgressBar($spatialPlannings->count()); - $progressBar->start(); - - $stats = [ - 'processed' => 0, - 'skipped' => 0, - 'errors' => 0, - 'created_proposals' => 0 - ]; - - foreach ($spatialPlannings as $spatial) { - try { - $result = $this->processSingleSpatialPlanning($spatial, $force); - - if ($result['status'] === 'processed') { - $stats['processed']++; - $stats['created_proposals'] += $result['proposals_created']; - } elseif ($result['status'] === 'skipped') { - $stats['skipped']++; - } else { - $stats['errors']++; - } - - } catch (Exception $e) { - $stats['errors']++; - $this->newLine(); - $this->error("Error processing spatial planning ID {$spatial->id}: " . $e->getMessage()); - } - - $progressBar->advance(); - } - - $progressBar->finish(); - $this->newLine(2); - - // Show final statistics - $this->showFinalStatistics($stats); - } - - /** - * Process single spatial planning - */ - private function processSingleSpatialPlanning(SpatialPlanning $spatial, $force) - { - // Check if already has proposals - if (!$force && $spatial->retributionProposals()->exists()) { - return ['status' => 'skipped', 'reason' => 'already_has_proposals']; - } - - // Detect building function - $buildingFunction = $this->proposalService->detectBuildingFunction($spatial->getBuildingFunctionText()); - if (!$buildingFunction) { - return ['status' => 'error', 'reason' => 'no_building_function']; - } - - // Get area - $totalArea = $spatial->getCalculationArea(); - if ($totalArea <= 0) { - return ['status' => 'error', 'reason' => 'no_area']; - } - - // Get number of floors - $numberOfFloors = max(1, $spatial->number_of_floors ?? 1); - - // Delete existing proposals if force mode - if ($force) { - $spatial->retributionProposals()->delete(); - } - - $proposalsCreated = 0; - - // Create proposals for each floor - for ($floor = 1; $floor <= $numberOfFloors; $floor++) { - // Calculate floor area (distribute total area across floors) - $floorArea = $totalArea / $numberOfFloors; - - // Create retribution proposal - $proposal = $this->proposalService->createProposalForSpatialPlanning( - $spatial, - $buildingFunction->id, - $floor, - $floorArea, - $totalArea, - "Auto-generated from spatial planning calculation" - ); - - if ($proposal) { - $proposalsCreated++; - } - } - - return [ - 'status' => 'processed', - 'proposals_created' => $proposalsCreated - ]; - } - - /** - * Show final statistics - */ - private function showFinalStatistics($stats) - { - $this->info('โœ… CALCULATION COMPLETED!'); - $this->newLine(); - - $this->table( - ['Metric', 'Count'], - [ - ['Processed Successfully', $stats['processed']], - ['Skipped (Already Exists)', $stats['skipped']], - ['Errors', $stats['errors']], - ['Total Proposals Created', $stats['created_proposals']], - ] - ); - - if ($stats['errors'] > 0) { - $this->newLine(); - $this->warn("โš ๏ธ {$stats['errors']} spatial plannings had errors:"); - $this->warn(" โ€ข Missing building function detection"); - $this->warn(" โ€ข Missing or zero area"); - $this->warn(" โ€ข Other calculation errors"); - } - - if ($stats['processed'] > 0) { - $this->newLine(); - $this->info("๐ŸŽ‰ Successfully created {$stats['created_proposals']} retribution proposals!"); - $this->info("๐Ÿ’ก You can view them using: php artisan retribution:list-proposals"); - } - } -} \ No newline at end of file diff --git a/app/Console/Commands/ProcessSpatialPlanningRetributionCommand.php b/app/Console/Commands/ProcessSpatialPlanningRetributionCommand.php deleted file mode 100644 index 96df416..0000000 --- a/app/Console/Commands/ProcessSpatialPlanningRetributionCommand.php +++ /dev/null @@ -1,321 +0,0 @@ -proposalService = $proposalService; - } - - /** - * Execute the console command. - */ - public function handle() - { - $this->info('๐Ÿ—๏ธ Processing Spatial Planning Retribution Calculations...'); - $this->newLine(); - - try { - // Get options - $processAll = $this->option('all'); - $newOnly = $this->option('new-only'); - $force = $this->option('force'); - - // Build query - $query = SpatialPlanning::query(); - - if ($newOnly || (!$processAll && !$force)) { - $query->whereDoesntHave('retributionProposals'); - $this->info('๐Ÿ“‹ Mode: Processing only spatial plannings WITHOUT retribution proposals'); - } elseif ($processAll) { - $this->info('๐Ÿ“‹ Mode: Processing ALL spatial plannings'); - } - - $spatialPlannings = $query->get(); - $totalCount = $spatialPlannings->count(); - - if ($totalCount === 0) { - $this->warn('โŒ No spatial plannings found to process.'); - $this->info('๐Ÿ’ก Try running: php artisan spatial:init first'); - return 0; - } - - $this->info("๐Ÿ“Š Found {$totalCount} spatial plannings to process"); - $this->newLine(); - - // Show preview of what will be processed - $this->showProcessingPreview($spatialPlannings); - - // Confirm processing - if (!$this->confirm("Process {$totalCount} spatial plannings and create retribution proposals?")) { - $this->info('Operation cancelled.'); - return 0; - } - - // Process all spatial plannings - $this->processAllSpatialPlannings($spatialPlannings, $force); - - return 0; - - } catch (Exception $e) { - $this->error('โŒ Error during processing: ' . $e->getMessage()); - return 1; - } - } - - /** - * Show processing preview - */ - private function showProcessingPreview($spatialPlannings) - { - $this->info('๐Ÿ” PREVIEW:'); - - $canProcess = 0; - $cannotProcess = 0; - $hasExisting = 0; - - $sampleData = []; - $errorReasons = []; - - foreach ($spatialPlannings->take(5) as $spatial) { - $buildingFunction = $this->proposalService->detectBuildingFunction($spatial->getBuildingFunctionText()); - $area = $spatial->getCalculationArea(); - $floors = max(1, $spatial->number_of_floors ?? 1); - $hasProposals = $spatial->retributionProposals()->exists(); - - if ($hasProposals) { - $hasExisting++; - } - - if ($buildingFunction && $area > 0) { - $canProcess++; - $sampleData[] = [ - 'ID' => $spatial->id, - 'Name' => substr($spatial->name ?? 'N/A', 0, 30), - 'Function' => $buildingFunction->name ?? 'N/A', - 'Area' => number_format($area, 2), - 'Floors' => $floors, - 'Status' => $hasProposals ? '๐Ÿ”„ Has Proposals' : 'โœ… Ready' - ]; - } else { - $cannotProcess++; - if (!$buildingFunction) { - $errorReasons[] = 'Missing building function'; - } - if ($area <= 0) { - $errorReasons[] = 'Missing/zero area'; - } - } - } - - // Show sample data table - if (!empty($sampleData)) { - $this->table( - ['ID', 'Name', 'Function', 'Area (mยฒ)', 'Floors', 'Status'], - $sampleData - ); - } - - // Show summary - $this->newLine(); - $this->info("๐Ÿ“ˆ SUMMARY:"); - $this->info(" โœ… Can Process: {$canProcess}"); - $this->info(" โŒ Cannot Process: {$cannotProcess}"); - $this->info(" ๐Ÿ”„ Has Existing Proposals: {$hasExisting}"); - - if ($cannotProcess > 0) { - $this->warn(" โš ๏ธ Common Issues: " . implode(', ', array_unique($errorReasons))); - } - - $this->newLine(); - } - - /** - * Process all spatial plannings - */ - private function processAllSpatialPlannings($spatialPlannings, $force) - { - $this->info('๐Ÿš€ Starting processing...'); - $this->newLine(); - - $progressBar = $this->output->createProgressBar($spatialPlannings->count()); - $progressBar->start(); - - $stats = [ - 'processed' => 0, - 'skipped' => 0, - 'errors' => 0, - 'total_proposals' => 0, - 'total_amount' => 0 - ]; - - $errors = []; - - foreach ($spatialPlannings as $spatial) { - try { - $result = $this->processSingleSpatialPlanning($spatial, $force); - - if ($result['success']) { - $stats['processed']++; - $stats['total_proposals'] += $result['proposals_count']; - $stats['total_amount'] += $result['total_amount']; - } elseif ($result['skipped']) { - $stats['skipped']++; - } else { - $stats['errors']++; - $errors[] = "ID {$spatial->id}: " . $result['error']; - } - - } catch (Exception $e) { - $stats['errors']++; - $errors[] = "ID {$spatial->id}: " . $e->getMessage(); - } - - $progressBar->advance(); - } - - $progressBar->finish(); - $this->newLine(2); - - // Show final results - $this->showFinalResults($stats, $errors); - } - - /** - * Process single spatial planning - */ - private function processSingleSpatialPlanning(SpatialPlanning $spatial, $force) - { - // Check if already has proposals - if (!$force && $spatial->retributionProposals()->exists()) { - return ['success' => false, 'skipped' => true]; - } - - // Detect building function - $buildingFunction = $this->proposalService->detectBuildingFunction($spatial->getBuildingFunctionText()); - if (!$buildingFunction) { - return ['success' => false, 'skipped' => false, 'error' => 'Cannot detect building function from: ' . $spatial->getBuildingFunctionText()]; - } - - // Get area - $totalArea = $spatial->getCalculationArea(); - if ($totalArea <= 0) { - return ['success' => false, 'skipped' => false, 'error' => 'Area is zero or missing']; - } - - // Get number of floors - $numberOfFloors = max(1, $spatial->number_of_floors ?? 1); - - // Delete existing proposals if force mode - if ($force) { - $spatial->retributionProposals()->delete(); - } - - $proposalsCount = 0; - $totalAmount = 0; - - // Create single proposal for the spatial planning (not per floor to prevent duplicates) - // Use the highest floor for IP ketinggian calculation (worst case scenario) - $highestFloor = $numberOfFloors; - - $proposal = $this->proposalService->createProposalForSpatialPlanning( - $spatial, - $buildingFunction->id, - $highestFloor, // Use highest floor for calculation - $totalArea, // Use total area, not divided by floors - $totalArea, - "Auto-calculated from spatial planning data (floors: {$numberOfFloors})" - ); - - if ($proposal) { - $proposalsCount = 1; - $totalAmount = $proposal->total_retribution_amount; - } - - return [ - 'success' => true, - 'skipped' => false, - 'proposals_count' => $proposalsCount, - 'total_amount' => $totalAmount - ]; - } - - /** - * Show final results - */ - private function showFinalResults($stats, $errors) - { - $this->info('๐ŸŽ‰ PROCESSING COMPLETED!'); - $this->newLine(); - - // Main statistics table - $this->table( - ['Metric', 'Count'], - [ - ['โœ… Successfully Processed', $stats['processed']], - ['โญ๏ธ Skipped (Already Has Proposals)', $stats['skipped']], - ['โŒ Errors', $stats['errors']], - ['๐Ÿ“„ Total Proposals Created', $stats['total_proposals']], - ['๐Ÿ’ฐ Total Retribution Amount', 'Rp ' . number_format($stats['total_amount'], 2)], - ] - ); - - // Show success message - if ($stats['processed'] > 0) { - $this->newLine(); - $this->info("๐ŸŽŠ SUCCESS!"); - $this->info(" ๐Ÿ“Š Created {$stats['total_proposals']} retribution proposals"); - $this->info(" ๐Ÿ’ต Total calculated amount: Rp " . number_format($stats['total_amount'], 2)); - $this->info(" ๐Ÿ“‹ Processed {$stats['processed']} spatial plannings"); - } - - // Show errors if any - if ($stats['errors'] > 0) { - $this->newLine(); - $this->warn("โš ๏ธ ERRORS ENCOUNTERED:"); - foreach (array_slice($errors, 0, 10) as $error) { - $this->warn(" โ€ข {$error}"); - } - - if (count($errors) > 10) { - $this->warn(" ... and " . (count($errors) - 10) . " more errors"); - } - } - - // Show next steps - $this->newLine(); - $this->info("๐Ÿ“‹ NEXT STEPS:"); - $this->info(" โ€ข View proposals: php artisan spatial:check-constraints"); - $this->info(" โ€ข Check database: SELECT COUNT(*) FROM retribution_proposals;"); - $this->info(" โ€ข Access via API: GET /api/retribution-proposals"); - } -} \ No newline at end of file diff --git a/app/Console/Commands/TestExcelFormulaCommand.php b/app/Console/Commands/TestExcelFormulaCommand.php deleted file mode 100644 index 168232d..0000000 --- a/app/Console/Commands/TestExcelFormulaCommand.php +++ /dev/null @@ -1,209 +0,0 @@ -info('๐Ÿงฎ Testing Excel Formula Calculation'); - $this->newLine(); - - // Get parameters - $buildingFunctionId = $this->option('building-function'); - $floorArea = (float) $this->option('floor-area'); - $floorNumber = (int) $this->option('floor-number'); - - // Get building function and parameters - $buildingFunction = BuildingFunction::with('parameter')->find($buildingFunctionId); - if (!$buildingFunction || !$buildingFunction->parameter) { - $this->error("Building function with ID {$buildingFunctionId} not found or has no parameters."); - return 1; - } - - // Get IP ketinggian for floor - $floorHeightIndex = FloorHeightIndex::where('floor_number', $floorNumber)->first(); - $ipKetinggian = $floorHeightIndex ? $floorHeightIndex->ip_ketinggian : 1.0; - - // Get parameters - $parameters = $buildingFunction->parameter->getParametersArray(); - - $this->info("Test Parameters:"); - $this->table( - ['Parameter', 'Excel Cell', 'Value'], - [ - ['Building Function', 'Building Function', $buildingFunction->name], - ['Floor Area (D13)', 'D13', $floorArea . ' mยฒ'], - ['Fungsi Bangunan (E13)', 'E13', $parameters['fungsi_bangunan']], - ['IP Permanen (F13)', 'F13', $parameters['ip_permanen']], - ['IP Kompleksitas (G13)', 'G13', $parameters['ip_kompleksitas']], - ['IP Ketinggian (H$3)', 'H$3', $ipKetinggian], - ['Indeks Lokalitas (N13)', 'N13', $parameters['indeks_lokalitas']], - ['Base Value', '70350', '70,350'], - ['Additional Factor ($O$3)', '$O$3', '0.5 (50%)'] - ] - ); - - $this->newLine(); - - // Calculate manually using the Excel formula - $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; - - // Create breakdown array - $breakdown = [ - 'calculation_steps' => [ - 'step_1_h13' => [ - 'formula' => 'fungsi_bangunan * (ip_permanen + ip_kompleksitas + (0.5 * ip_ketinggian))', - 'calculation' => "{$fungsi_bangunan} * ({$ip_permanen} + {$ip_kompleksitas} + (0.5 * {$ipKetinggian}))", - 'result' => $h13 - ], - 'step_2_main' => [ - 'formula' => '(1*D13*(N13*70350*H13*1))', - 'calculation' => "1 * {$floorArea} * ({$indeks_lokalitas} * {$base_value} * {$h13} * 1)", - 'result' => $main_calculation - ], - 'step_3_additional' => [ - 'formula' => '($O$3*(1*D13*(N13*70350*H13*1)))', - 'calculation' => "{$additional_factor} * {$main_calculation}", - 'result' => $additional_calculation - ], - 'step_4_total' => [ - 'formula' => 'main + additional', - 'calculation' => "{$main_calculation} + {$additional_calculation}", - 'result' => $total_retribution - ] - ], - 'formatted_results' => [ - 'H13' => number_format($h13, 6), - 'main_calculation' => 'Rp ' . number_format($main_calculation, 2), - 'additional_calculation' => 'Rp ' . number_format($additional_calculation, 2), - 'total_result' => 'Rp ' . number_format($total_retribution, 2) - ] - ]; - - $this->info("๐Ÿ“Š Calculation Breakdown:"); - $this->newLine(); - - // Show each step - foreach ($breakdown['calculation_steps'] as $stepName => $step) { - $this->info("๐Ÿ”ธ " . strtoupper(str_replace('_', ' ', $stepName))); - $this->line(" Formula: " . $step['formula']); - $this->line(" Calculation: " . $step['calculation']); - $this->line(" Result: " . (is_numeric($step['result']) ? number_format($step['result'], 6) : $step['result'])); - $this->newLine(); - } - - $this->info("๐Ÿ’ฐ Final Results:"); - $this->table( - ['Component', 'Value'], - [ - ['H13 (Floor Coefficient)', $breakdown['formatted_results']['H13']], - ['Main Calculation', $breakdown['formatted_results']['main_calculation']], - ['Additional (50%)', $breakdown['formatted_results']['additional_calculation']], - ['Total Retribution', $breakdown['formatted_results']['total_result']] - ] - ); - - $this->newLine(); - $this->info("๐Ÿ” Excel Formula Verification:"); - $this->line("Main Formula: =(1*D13*(N13*70350*H13*1))+(\$O\$3*(1*D13*(N13*70350*H13*1)))"); - $this->line("H13 Formula: =(\$E13*(\$F13+\$G13+(0.5*H\$3)))"); - - // Test with different floor numbers - if ($this->confirm('Test with different floor numbers?')) { - $this->testMultipleFloors($buildingFunction, $floorArea, $parameters); - } - - return 0; - } - - /** - * Test calculation with multiple floor numbers - */ - protected function testMultipleFloors(BuildingFunction $buildingFunction, float $floorArea, array $parameters) - { - $this->newLine(); - $this->info("๐Ÿข Testing Multiple Floors:"); - - $tableData = []; - $totalRetribution = 0; - - for ($floor = 1; $floor <= 5; $floor++) { - $floorHeightIndex = FloorHeightIndex::where('floor_number', $floor)->first(); - $ipKetinggian = $floorHeightIndex ? $floorHeightIndex->ip_ketinggian : 1.0; - - // Calculate using Excel formula - $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; - - // Calculate H13 and result - $H13 = $fungsi_bangunan * ($ip_permanen + $ip_kompleksitas + (0.5 * $ipKetinggian)); - $main_calc = 1 * $floorArea * ($indeks_lokalitas * $base_value * $H13 * 1); - $additional_calc = $additional_factor * $main_calc; - $result = $main_calc + $additional_calc; - - $totalRetribution += $result; - - $tableData[] = [ - "L{$floor}", - $ipKetinggian, - number_format($H13, 6), - 'Rp ' . number_format($result, 2) - ]; - } - - $this->table( - ['Floor', 'IP Ketinggian', 'H13 Value', 'Retribution Amount'], - $tableData - ); - - $this->newLine(); - $this->info("๐Ÿ—๏ธ Total Building Retribution (5 floors): Rp " . number_format($totalRetribution, 2)); - $this->info("๐Ÿ“ Total Building Area: " . number_format($floorArea * 5, 2) . " mยฒ"); - $this->info("๐Ÿ’ก Average per mยฒ: Rp " . number_format($totalRetribution / ($floorArea * 5), 2)); - } -} \ No newline at end of file diff --git a/app/Console/Commands/TestRetributionCalculation.php b/app/Console/Commands/TestRetributionCalculation.php index 88fd30b..369a5dd 100644 --- a/app/Console/Commands/TestRetributionCalculation.php +++ b/app/Console/Commands/TestRetributionCalculation.php @@ -150,9 +150,11 @@ class TestRetributionCalculation extends Command protected function performCalculation($buildingTypeId, $floor, $area) { try { - $result = $this->calculatorService->calculate($buildingTypeId, $floor, $area, false); + // Round area to 2 decimal places to match database storage format + $roundedArea = round($area, 2); + $result = $this->calculatorService->calculate($buildingTypeId, $floor, $roundedArea, false); - $this->displayResults($result, $area, $floor); + $this->displayResults($result, $roundedArea, $floor); } catch (\Exception $e) { $this->error('โŒ Error: ' . $e->getMessage()); @@ -208,7 +210,7 @@ class TestRetributionCalculation extends Command protected function testAllBuildingTypes() { - $area = $this->option('area') ?: 100; + $area = round($this->option('area') ?: 100, 2); $floor = $this->option('floor') ?: 2; $this->info("๐Ÿงช TESTING SEMUA BUILDING TYPES"); diff --git a/app/Console/Commands/TestRetributionData.php b/app/Console/Commands/TestRetributionData.php deleted file mode 100644 index cf64150..0000000 --- a/app/Console/Commands/TestRetributionData.php +++ /dev/null @@ -1,291 +0,0 @@ -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/Http/Controllers/RetributionProposalController.php b/app/Http/Controllers/RetributionProposalController.php deleted file mode 100644 index 1988377..0000000 --- a/app/Http/Controllers/RetributionProposalController.php +++ /dev/null @@ -1,295 +0,0 @@ -proposalService = $proposalService; - } - - /** - * Display a listing of retribution proposals - */ - public function index(Request $request): JsonResponse - { - $query = RetributionProposal::with(['spatialPlanning', 'buildingFunction', 'retributionFormula']); - - // Filtering - if ($request->has('building_function_id')) { - $query->where('building_function_id', $request->building_function_id); - } - - if ($request->has('spatial_planning_id')) { - $query->where('spatial_planning_id', $request->spatial_planning_id); - } - - if ($request->has('floor_number')) { - $query->where('floor_number', $request->floor_number); - } - - if ($request->has('has_spatial_planning')) { - if ($request->boolean('has_spatial_planning')) { - $query->whereNotNull('spatial_planning_id'); - } else { - $query->whereNull('spatial_planning_id'); - } - } - - // Search - if ($request->has('search')) { - $search = $request->search; - $query->where(function ($q) use ($search) { - $q->where('proposal_number', 'like', "%{$search}%") - ->orWhere('notes', 'like', "%{$search}%") - ->orWhereHas('spatialPlanning', function ($sq) use ($search) { - $sq->where('name', 'like', "%{$search}%"); - }) - ->orWhereHas('buildingFunction', function ($bq) use ($search) { - $bq->where('name', 'like', "%{$search}%"); - }); - }); - } - - // Sorting - $sortBy = $request->get('sort_by', 'created_at'); - $sortOrder = $request->get('sort_order', 'desc'); - $query->orderBy($sortBy, $sortOrder); - - // Pagination - $perPage = $request->get('per_page', 15); - $proposals = $query->paginate($perPage); - - return response()->json([ - 'success' => true, - 'data' => $proposals, - 'meta' => [ - 'total_amount' => RetributionProposal::sum('total_retribution_amount'), - 'total_proposals' => RetributionProposal::count(), - 'formatted_total_amount' => 'Rp ' . number_format(RetributionProposal::sum('total_retribution_amount'), 0, ',', '.') - ] - ]); - } - - /** - * Store a new retribution proposal - */ - public function store(Request $request): JsonResponse - { - $request->validate([ - 'spatial_planning_id' => 'nullable|exists:spatial_plannings,id', - 'building_function_id' => 'required|exists:building_functions,id', - 'floor_number' => 'required|integer|min:1|max:10', - 'floor_area' => 'required|numeric|min:0.01', - 'total_building_area' => 'nullable|numeric|min:0.01', - 'notes' => 'nullable|string|max:1000' - ]); - - try { - if ($request->spatial_planning_id) { - $spatialPlanning = SpatialPlanning::findOrFail($request->spatial_planning_id); - $proposal = $this->proposalService->createProposalForSpatialPlanning( - $spatialPlanning, - $request->building_function_id, - $request->floor_number, - $request->floor_area, - $request->total_building_area, - $request->notes - ); - } else { - $proposal = $this->proposalService->createStandaloneProposal( - $request->building_function_id, - $request->floor_number, - $request->floor_area, - $request->total_building_area, - $request->notes - ); - } - - $proposal->load(['spatialPlanning', 'buildingFunction', 'retributionFormula']); - - return response()->json([ - 'success' => true, - 'message' => 'Retribution proposal created successfully', - 'data' => $proposal - ], 201); - - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to create retribution proposal', - 'error' => $e->getMessage() - ], 400); - } - } - - /** - * Display the specified retribution proposal - */ - public function show(int $id): JsonResponse - { - $proposal = RetributionProposal::with(['spatialPlanning', 'buildingFunction', 'retributionFormula']) - ->findOrFail($id); - - return response()->json([ - 'success' => true, - 'data' => $proposal - ]); - } - - /** - * Update the specified retribution proposal - */ - public function update(Request $request, int $id): JsonResponse - { - $proposal = RetributionProposal::findOrFail($id); - - $request->validate([ - 'notes' => 'nullable|string|max:1000' - ]); - - $proposal->update($request->only(['notes'])); - - $proposal->load(['spatialPlanning', 'buildingFunction', 'retributionFormula']); - - return response()->json([ - 'success' => true, - 'message' => 'Retribution proposal updated successfully', - 'data' => $proposal - ]); - } - - /** - * Remove the specified retribution proposal - */ - public function destroy(int $id): JsonResponse - { - $proposal = RetributionProposal::findOrFail($id); - $proposal->delete(); - - return response()->json([ - 'success' => true, - 'message' => 'Retribution proposal deleted successfully' - ]); - } - - /** - * Get retribution proposal statistics - */ - public function statistics(): JsonResponse - { - $stats = $this->proposalService->getStatistics(); - - return response()->json([ - 'success' => true, - 'data' => $stats - ]); - } - - /** - * Get total sum of all retribution proposals - */ - public function totalSum(): JsonResponse - { - $totalAmount = RetributionProposal::sum('total_retribution_amount'); - - return response()->json([ - 'success' => true, - 'data' => [ - 'total_amount' => $totalAmount, - 'formatted_total_amount' => 'Rp ' . number_format($totalAmount, 0, ',', '.'), - 'total_proposals' => RetributionProposal::count() - ] - ]); - } - - /** - * Get building functions list - */ - public function buildingFunctions(): JsonResponse - { - $buildingFunctions = BuildingFunction::whereNotNull('parent_id') - ->with(['parameter']) - ->orderBy('name') - ->get(); - - return response()->json([ - 'success' => true, - 'data' => $buildingFunctions - ]); - } - - /** - * Auto-detect building function and create proposal - */ - public function autoCreateProposal(Request $request): JsonResponse - { - $request->validate([ - 'spatial_planning_id' => 'nullable|exists:spatial_plannings,id', - 'building_function_text' => 'required|string', - 'floor_number' => 'required|integer|min:1|max:10', - 'floor_area' => 'required|numeric|min:0.01', - 'total_building_area' => 'nullable|numeric|min:0.01', - 'notes' => 'nullable|string|max:1000' - ]); - - try { - // Auto-detect building function - $buildingFunction = $this->proposalService->detectBuildingFunction($request->building_function_text); - - if (!$buildingFunction) { - return response()->json([ - 'success' => false, - 'message' => 'Could not detect building function from text', - 'suggestion' => 'Please specify building function manually' - ], 400); - } - - if ($request->spatial_planning_id) { - $spatialPlanning = SpatialPlanning::findOrFail($request->spatial_planning_id); - $proposal = $this->proposalService->createProposalForSpatialPlanning( - $spatialPlanning, - $buildingFunction->id, - $request->floor_number, - $request->floor_area, - $request->total_building_area, - $request->notes - ); - } else { - $proposal = $this->proposalService->createStandaloneProposal( - $buildingFunction->id, - $request->floor_number, - $request->floor_area, - $request->total_building_area, - $request->notes - ); - } - - $proposal->load(['spatialPlanning', 'buildingFunction', 'retributionFormula']); - - return response()->json([ - 'success' => true, - 'message' => 'Retribution proposal created successfully with auto-detected building function', - 'data' => $proposal, - 'detected_building_function' => $buildingFunction->name - ], 201); - - } catch (\Exception $e) { - return response()->json([ - 'success' => false, - 'message' => 'Failed to create retribution proposal', - 'error' => $e->getMessage() - ], 400); - } - } -} \ No newline at end of file diff --git a/app/Models/BuildingFunction.php b/app/Models/BuildingFunction.php deleted file mode 100644 index d384aea..0000000 --- a/app/Models/BuildingFunction.php +++ /dev/null @@ -1,167 +0,0 @@ - 'integer', - 'sort_order' => 'integer', - 'base_tariff' => 'decimal:2' - ]; - - /** - * Parent relationship (self-referencing) - */ - public function parent(): BelongsTo - { - return $this->belongsTo(BuildingFunction::class, 'parent_id'); - } - - /** - * Children relationship (self-referencing) - */ - public function children(): HasMany - { - return $this->hasMany(BuildingFunction::class, 'parent_id') - ->orderBy('sort_order'); - } - - /** - * Parameters relationship (1:1) - */ - public function parameter(): HasOne - { - return $this->hasOne(BuildingFunctionParameter::class); - } - - /** - * Formulas relationship (1:n) - multiple formulas for different floor numbers - */ - public function formulas(): HasMany - { - return $this->hasMany(RetributionFormula::class)->orderBy('floor_number'); - } - - /** - * Get appropriate formula based on floor number - */ - public function getFormulaForFloor(int $floorNumber): ?RetributionFormula - { - return $this->formulas() - ->where('floor_number', $floorNumber) - ->first(); - } - - /** - * Retribution proposals relationship (1:n) - */ - public function retributionProposals(): HasMany - { - return $this->hasMany(RetributionProposal::class, 'building_function_id'); - } - - /** - * Scope: Parent functions only - */ - public function scopeParents($query) - { - return $query->whereNull('parent_id'); - } - - /** - * Scope: Children functions only - */ - public function scopeChildren($query) - { - return $query->whereNotNull('parent_id'); - } - - /** - * Scope: By level - */ - public function scopeByLevel($query, int $level) - { - return $query->where('level', $level); - } - - /** - * Check if building function has complete setup (parameters + at least one formula) - */ - public function hasCompleteSetup(): bool - { - return $this->parameter()->exists() && $this->formulas()->exists(); - } - - /** - * Get building function with all related data - */ - public function getCompleteData(): array - { - return [ - 'id' => $this->id, - 'code' => $this->code, - 'name' => $this->name, - 'description' => $this->description, - 'level' => $this->level, - 'sort_order' => $this->sort_order, - 'base_tariff' => $this->base_tariff, - 'parameters' => $this->parameter?->getParametersArray(), - 'formulas' => $this->formulas->map(function ($formula) { - return [ - 'id' => $formula->id, - 'name' => $formula->name, - 'expression' => $formula->formula_expression, - 'floor_number' => $formula->floor_number, - 'floor_description' => $this->getFloorDescription($formula->floor_number) - ]; - })->toArray(), - 'has_complete_setup' => $this->hasCompleteSetup() - ]; - } - - /** - * Get floor description - */ - public function getFloorDescription(?int $floorNumber): string - { - if ($floorNumber === null || $floorNumber === 0) { - return 'Semua lantai'; - } - - return "Lantai {$floorNumber}"; - } - - /** - * Check if this is a parent function - */ - public function isParent(): bool - { - return $this->parent_id === null; - } - - /** - * Check if this is a child function - */ - public function isChild(): bool - { - return $this->parent_id !== null; - } -} diff --git a/app/Models/BuildingFunctionParameter.php b/app/Models/BuildingFunctionParameter.php deleted file mode 100644 index 3d561db..0000000 --- a/app/Models/BuildingFunctionParameter.php +++ /dev/null @@ -1,134 +0,0 @@ - 'decimal:6', - 'ip_permanen' => 'decimal:6', - 'ip_kompleksitas' => 'decimal:6', - 'indeks_lokalitas' => 'decimal:6', - 'asumsi_prasarana' => 'decimal:6', - 'koefisien_dasar' => 'decimal:6', - 'faktor_penyesuaian' => 'decimal:6', - 'custom_parameters' => 'array' - ]; - - /** - * Building function relationship (1:1) - */ - public function buildingFunction(): BelongsTo - { - return $this->belongsTo(BuildingFunction::class); - } - - /** - * Scope: Active parameters only - */ - public function scopeActive($query) - { - return $query->where('is_active', true); - } - - /** - * Get all parameter values as array - */ - public function getParametersArray(): array - { - return [ - 'fungsi_bangunan' => $this->fungsi_bangunan, - 'ip_permanen' => $this->ip_permanen, - 'ip_kompleksitas' => $this->ip_kompleksitas, - 'indeks_lokalitas' => $this->indeks_lokalitas, - 'asumsi_prasarana' => $this->asumsi_prasarana, - 'koefisien_dasar' => $this->koefisien_dasar, - 'faktor_penyesuaian' => $this->faktor_penyesuaian, - 'custom_parameters' => $this->custom_parameters - ]; - } - - /** - * Get formatted parameters for display - */ - public function getFormattedParameters(): array - { - return [ - 'Fungsi Bangunan' => $this->fungsi_bangunan, - 'IP Permanen' => $this->ip_permanen, - 'IP Kompleksitas' => $this->ip_kompleksitas, - 'Indeks Lokalitas' => $this->indeks_lokalitas, - 'Asumsi Prasarana' => $this->asumsi_prasarana, - 'Koefisien Dasar' => $this->koefisien_dasar, - 'Faktor Penyesuaian' => $this->faktor_penyesuaian - ]; - } - - /** - * Calculate floor result using the main formula - */ - public function calculateFloorResult(float $ipKetinggian): float - { - return $this->fungsi_bangunan * ( - $this->ip_permanen + - $this->ip_kompleksitas + - (0.5 * $ipKetinggian) - ); - } - - /** - * Calculate full retribution for given building area and floor result - */ - public function calculateRetribution(float $luasBangunan, float $floorResult): float - { - $baseValue = 70350; // Base retribution value - $mainCalculation = 1 * $luasBangunan * ($this->indeks_lokalitas * $baseValue * $floorResult * 1); - $additionalCalculation = 0.5 * $mainCalculation; - - return $mainCalculation + $additionalCalculation; - } - - /** - * Apply custom parameters if available - */ - public function getParameterValue(string $key, $default = null) - { - // First check if it's a standard parameter - if (property_exists($this, $key) && $this->$key !== null) { - return $this->$key; - } - - // Then check custom parameters - if ($this->custom_parameters && is_array($this->custom_parameters)) { - return $this->custom_parameters[$key] ?? $default; - } - - return $default; - } - - /** - * Set custom parameter - */ - public function setCustomParameter(string $key, $value): void - { - $customParams = $this->custom_parameters ?? []; - $customParams[$key] = $value; - $this->custom_parameters = $customParams; - } -} diff --git a/app/Models/CalculableRetribution.php b/app/Models/CalculableRetribution.php new file mode 100644 index 0000000..bfc5b5f --- /dev/null +++ b/app/Models/CalculableRetribution.php @@ -0,0 +1,64 @@ + 'boolean', + 'assigned_at' => 'timestamp', + ]; + + /** + * Get the owning calculable model (polymorphic) + */ + public function calculable(): MorphTo + { + return $this->morphTo(); + } + + /** + * Get the retribution calculation + */ + public function retributionCalculation(): BelongsTo + { + return $this->belongsTo(RetributionCalculation::class); + } + + /** + * Scope: Only active assignments + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + /** + * Scope: Only inactive assignments + */ + public function scopeInactive($query) + { + return $query->where('is_active', false); + } + + /** + * Scope: For specific calculable type + */ + public function scopeForType($query, string $type) + { + return $query->where('calculable_type', $type); + } +} diff --git a/app/Models/FloorHeightIndex.php b/app/Models/FloorHeightIndex.php deleted file mode 100644 index 2b25e33..0000000 --- a/app/Models/FloorHeightIndex.php +++ /dev/null @@ -1,56 +0,0 @@ - 'integer', - 'ip_ketinggian' => 'decimal:6' - ]; - - /** - * Get IP ketinggian by floor number - */ - public static function getIpKetinggianByFloor(int $floorNumber): float - { - $index = self::where('floor_number', $floorNumber)->first(); - return $index ? (float) $index->ip_ketinggian : 1.0; - } - - /** - * Get all IP ketinggian mapping as array - */ - public static function getAllMapping(): array - { - return self::orderBy('floor_number') - ->pluck('ip_ketinggian', 'floor_number') - ->toArray(); - } - - /** - * Get available floor numbers - */ - public static function getAvailableFloors(): array - { - return self::orderBy('floor_number') - ->pluck('floor_number') - ->toArray(); - } - - /** - * Scope: By floor number - */ - public function scopeByFloor($query, int $floorNumber) - { - return $query->where('floor_number', $floorNumber); - } -} diff --git a/app/Models/RetributionCalculation.php b/app/Models/RetributionCalculation.php index 5d38474..3bdd742 100644 --- a/app/Models/RetributionCalculation.php +++ b/app/Models/RetributionCalculation.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Carbon\Carbon; class RetributionCalculation extends Model @@ -15,23 +16,39 @@ class RetributionCalculation extends Model 'building_area', 'retribution_amount', 'calculation_detail', - 'calculated_at' + 'calculated_at', ]; protected $casts = [ - 'floor_number' => 'integer', 'building_area' => 'decimal:2', 'retribution_amount' => 'decimal:2', 'calculation_detail' => 'array', - 'calculated_at' => 'datetime' + 'calculated_at' => 'timestamp', + 'floor_number' => 'integer', ]; /** - * Building type relationship + * Get the building type */ public function buildingType(): BelongsTo { - return $this->belongsTo(BuildingType::class, 'building_type_id'); + return $this->belongsTo(BuildingType::class); + } + + /** + * Get all calculable assignments + */ + public function calculableRetributions(): HasMany + { + return $this->hasMany(CalculableRetribution::class); + } + + /** + * Get active assignments only + */ + public function activeAssignments(): HasMany + { + return $this->hasMany(CalculableRetribution::class)->where('is_active', true); } /** @@ -39,7 +56,48 @@ class RetributionCalculation extends Model */ public static function generateCalculationId(): string { - return 'RTB' . Carbon::now()->format('ymdHis') . rand(10, 99); + return 'CALC-' . date('Ymd') . '-' . str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT); + } + + /** + * Boot method to auto-generate calculation_id + */ + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (empty($model->calculation_id)) { + $model->calculation_id = self::generateCalculationId(); + } + if (empty($model->calculated_at)) { + $model->calculated_at = now(); + } + }); + } + + /** + * Check if calculation is being used + */ + public function isInUse(): bool + { + return $this->activeAssignments()->exists(); + } + + /** + * Get calculation summary + */ + public function getSummary(): array + { + return [ + 'calculation_id' => $this->calculation_id, + 'building_type' => $this->buildingType->name ?? 'Unknown', + 'floor_number' => $this->floor_number, + 'building_area' => $this->building_area, + 'retribution_amount' => $this->retribution_amount, + 'calculated_at' => $this->calculated_at->format('Y-m-d H:i:s'), + 'in_use' => $this->isInUse(), + ]; } /** diff --git a/app/Models/RetributionFormula.php b/app/Models/RetributionFormula.php deleted file mode 100644 index 527608e..0000000 --- a/app/Models/RetributionFormula.php +++ /dev/null @@ -1,227 +0,0 @@ - 'integer' - ]; - - /** - * Building function relationship (n:1) - */ - public function buildingFunction(): BelongsTo - { - return $this->belongsTo(BuildingFunction::class); - } - - /** - * Retribution proposals relationship (1:n) - */ - public function retributionProposals(): HasMany - { - return $this->hasMany(RetributionProposal::class); - } - - /** - * Scope: By floor number - */ - public function scopeByFloor($query, int $floorNumber) - { - return $query->where('floor_number', $floorNumber); - } - - /** - * Execute formula calculation with parameters and IP ketinggian - */ - public function calculate(float $luasBangunan, array $parameters, float $ipKetinggian): float - { - // Extract parameters - $fungsi_bangunan = $parameters['fungsi_bangunan'] ?? 0; - $ip_permanen = $parameters['ip_permanen'] ?? 0; - $ip_kompleksitas = $parameters['ip_kompleksitas'] ?? 0; - $indeks_lokalitas = $parameters['indeks_lokalitas'] ?? 0; - - // Calculate H13 (floor coefficient) using Excel formula - $h13 = $fungsi_bangunan * ($ip_permanen + $ip_kompleksitas + (0.5 * $ipKetinggian)); - - // Calculate full retribution using Excel formula - $baseValue = 70350; - $additionalFactor = 0.5; - - // Main calculation: (1*D13*(N13*70350*H13*1)) - $mainCalculation = 1 * $luasBangunan * ($indeks_lokalitas * $baseValue * $h13 * 1); - - // Additional calculation: ($O$3*(1*D13*(N13*70350*H13*1))) - $additionalCalculation = $additionalFactor * $mainCalculation; - - // Total: main + additional - return $mainCalculation + $additionalCalculation; - } - - /** - * Get formula with parameter placeholders replaced for display - */ - public function getDisplayFormula(array $parameters = []): string - { - $formula = $this->formula_expression; - - if (!empty($parameters)) { - foreach ($parameters as $key => $value) { - $formula = str_replace($key, "({$key}={$value})", $formula); - } - } - - return $formula; - } - - /** - * Check if this formula applies to given floor number - */ - public function appliesTo(int $floorNumber): bool - { - // If floor_number is 0, formula applies to all floors - if ($this->floor_number === 0) { - return true; - } - - // Otherwise, exact match required - return $this->floor_number == $floorNumber; - } - - /** - * Get human readable floor description - */ - public function getFloorDescription(): string - { - if ($this->floor_number === 0) { - return 'Semua lantai'; - } - - return "Lantai {$this->floor_number}"; - } - - /** - * Get default formula expression - */ - public static function getDefaultFormula(): string - { - return '(fungsi_bangunan * (ip_permanen + ip_kompleksitas + (0.5 * ip_ketinggian)))'; - } - - /** - * Validate formula expression syntax - */ - public function validateFormula(): bool - { - // Basic validation - check if formula contains required variables - $requiredVariables = ['fungsi_bangunan', 'ip_permanen', 'ip_kompleksitas', 'ip_ketinggian']; - - foreach ($requiredVariables as $variable) { - if (strpos($this->formula_expression, $variable) === false) { - return false; - } - } - - return true; - } - - /** - * Get calculation breakdown for debugging - */ - public function getCalculationBreakdown(float $luasBangunan, array $parameters, float $ipKetinggian): array - { - // Extract parameters - $fungsi_bangunan = $parameters['fungsi_bangunan'] ?? 0; - $ip_permanen = $parameters['ip_permanen'] ?? 0; - $ip_kompleksitas = $parameters['ip_kompleksitas'] ?? 0; - $indeks_lokalitas = $parameters['indeks_lokalitas'] ?? 0; - - // Calculate H13 (floor coefficient) - $h13 = $fungsi_bangunan * ($ip_permanen + $ip_kompleksitas + (0.5 * $ipKetinggian)); - - // Calculate components - $baseValue = 70350; - $additionalFactor = 0.5; - $mainCalculation = 1 * $luasBangunan * ($indeks_lokalitas * $baseValue * $h13 * 1); - $additionalCalculation = $additionalFactor * $mainCalculation; - $totalCalculation = $mainCalculation + $additionalCalculation; - - return [ - 'input_parameters' => [ - 'luas_bangunan' => $luasBangunan, - 'fungsi_bangunan' => $fungsi_bangunan, - 'ip_permanen' => $ip_permanen, - 'ip_kompleksitas' => $ip_kompleksitas, - 'ip_ketinggian' => $ipKetinggian, - 'indeks_lokalitas' => $indeks_lokalitas, - 'base_value' => $baseValue, - 'additional_factor' => $additionalFactor - ], - 'calculation_steps' => [ - '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 * luas_bangunan * (indeks_lokalitas * base_value * h13 * 1)', - 'calculation' => "1 * {$luasBangunan} * ({$indeks_lokalitas} * {$baseValue} * {$h13} * 1)", - 'result' => $mainCalculation - ], - 'additional_calculation' => [ - 'formula' => 'additional_factor * main_calculation', - 'calculation' => "{$additionalFactor} * {$mainCalculation}", - 'result' => $additionalCalculation - ], - 'total_calculation' => [ - 'formula' => 'main_calculation + additional_calculation', - 'calculation' => "{$mainCalculation} + {$additionalCalculation}", - 'result' => $totalCalculation - ] - ], - 'formatted_results' => [ - 'h13' => number_format($h13, 6), - 'main_calculation' => 'Rp ' . number_format($mainCalculation, 2), - 'additional_calculation' => 'Rp ' . number_format($additionalCalculation, 2), - 'total_calculation' => 'Rp ' . number_format($totalCalculation, 2) - ] - ]; - } - - /** - * Check if this formula has been used in any proposals - */ - public function hasProposals(): bool - { - return $this->retributionProposals()->exists(); - } - - /** - * Get total amount calculated using this formula - */ - public function getTotalCalculatedAmount(): float - { - return $this->retributionProposals()->sum('total_retribution_amount'); - } - - /** - * Get count of proposals using this formula - */ - public function getProposalCount(): int - { - return $this->retributionProposals()->count(); - } -} diff --git a/app/Models/RetributionProposal.php b/app/Models/RetributionProposal.php deleted file mode 100644 index 4fa42db..0000000 --- a/app/Models/RetributionProposal.php +++ /dev/null @@ -1,187 +0,0 @@ - 'decimal:6', - 'total_building_area' => 'decimal:6', - 'ip_ketinggian' => 'decimal:6', - 'floor_retribution_amount' => 'decimal:2', - 'total_retribution_amount' => 'decimal:2', - 'calculation_parameters' => 'array', - 'calculation_breakdown' => 'array', - 'calculated_at' => 'datetime' - ]; - - - - /** - * Relationship with SpatialPlanning - */ - public function spatialPlanning(): BelongsTo - { - return $this->belongsTo(SpatialPlanning::class); - } - - /** - * Relationship with BuildingFunction - */ - public function buildingFunction(): BelongsTo - { - return $this->belongsTo(BuildingFunction::class); - } - - /** - * Relationship with RetributionFormula - */ - public function retributionFormula(): BelongsTo - { - return $this->belongsTo(RetributionFormula::class); - } - - /** - * Generate proposal number - */ - public static function generateProposalNumber(): string - { - $year = now()->format('Y'); - $month = now()->format('m'); - - // Use max ID + 1 to avoid duplicates when records are deleted - $maxId = static::whereYear('created_at', now()->year) - ->whereMonth('created_at', now()->month) - ->max('id') ?? 0; - $nextNumber = $maxId + 1; - - // Fallback: if still duplicate, use timestamp - $proposalNumber = sprintf('RP-%s%s-%04d', $year, $month, $nextNumber); - - // Check if exists and increment until unique - $counter = $nextNumber; - while (static::where('proposal_number', $proposalNumber)->exists()) { - $counter++; - $proposalNumber = sprintf('RP-%s%s-%04d', $year, $month, $counter); - } - - return $proposalNumber; - } - - /** - * Get formatted floor retribution amount - */ - public function getFormattedFloorAmountAttribute(): string - { - return 'Rp ' . number_format($this->floor_retribution_amount, 0, ',', '.'); - } - - /** - * Get formatted total retribution amount - */ - public function getFormattedTotalAmountAttribute(): string - { - return 'Rp ' . number_format($this->total_retribution_amount, 0, ',', '.'); - } - - /** - * Get calculation breakdown for specific parameter - */ - public function getCalculationBreakdownFor(string $parameter) - { - return $this->calculation_breakdown[$parameter] ?? null; - } - - /** - * Get parameter value - */ - public function getParameterValue(string $parameter) - { - return $this->calculation_parameters[$parameter] ?? null; - } - - - - - - /** - * Scope for filtering by building function - */ - public function scopeByBuildingFunction($query, int $buildingFunctionId) - { - return $query->where('building_function_id', $buildingFunctionId); - } - - /** - * Scope for filtering by spatial planning - */ - public function scopeBySpatialPlanning($query, int $spatialPlanningId) - { - return $query->where('spatial_planning_id', $spatialPlanningId); - } - - /** - * Get total retribution amount for multiple proposals - */ - public static function getTotalAmount($proposals = null): float - { - if ($proposals) { - return $proposals->sum('total_retribution_amount'); - } - - return static::sum('total_retribution_amount'); - } - - /** - * Get basic statistics - */ - public static function getBasicStats(): array - { - return [ - 'total_count' => static::count(), - 'total_amount' => static::sum('total_retribution_amount'), - 'average_amount' => static::avg('total_retribution_amount'), - ]; - } - - /** - * Boot method to generate proposal number - */ - protected static function boot() - { - parent::boot(); - - static::creating(function ($model) { - if (empty($model->proposal_number)) { - $model->proposal_number = static::generateProposalNumber(); - } - - if (empty($model->calculated_at)) { - $model->calculated_at = now(); - } - }); - } -} \ No newline at end of file diff --git a/app/Models/SpatialPlanning.php b/app/Models/SpatialPlanning.php index 5807924..7e8fda9 100644 --- a/app/Models/SpatialPlanning.php +++ b/app/Models/SpatialPlanning.php @@ -2,10 +2,9 @@ namespace App\Models; +use App\Traits\HasRetributionCalculation; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\HasOne; -use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\BelongsTo; + /** * Class SpatialPlanning @@ -26,6 +25,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; */ class SpatialPlanning extends Model { + use HasRetributionCalculation; protected $perPage = 20; @@ -44,45 +44,7 @@ class SpatialPlanning extends Model 'date' => 'date' ]; - /** - * Retribution proposals relationship (1:many) - */ - public function retributionProposals(): HasMany - { - return $this->hasMany(RetributionProposal::class); - } - /** - * Building function relationship (if building_function becomes FK in future) - */ - public function buildingFunctionRelation(): BelongsTo - { - return $this->belongsTo(BuildingFunction::class, 'building_function_id'); - } - - /** - * Check if spatial planning has retribution proposals - */ - public function hasRetributionProposals(): bool - { - return $this->retributionProposals()->exists(); - } - - /** - * Get latest retribution proposal - */ - public function getLatestRetributionProposal() - { - return $this->retributionProposals()->latest()->first(); - } - - /** - * Get all retribution proposals - */ - public function getAllRetributionProposals() - { - return $this->retributionProposals()->get(); - } /** * Get building function text for detection @@ -100,21 +62,7 @@ class SpatialPlanning extends Model return (float) ($this->area ?? $this->land_area ?? 0); } - /** - * Scope: Without retribution proposals - */ - public function scopeWithoutRetributionProposals($query) - { - return $query->whereDoesntHave('retributionProposals'); - } - /** - * Scope: With retribution proposals - */ - public function scopeWithRetributionProposals($query) - { - return $query->whereHas('retributionProposals'); - } } diff --git a/app/Services/RetributionCalculatorService.php b/app/Services/RetributionCalculatorService.php index 19a4093..b4997e9 100644 --- a/app/Services/RetributionCalculatorService.php +++ b/app/Services/RetributionCalculatorService.php @@ -16,7 +16,8 @@ class RetributionCalculatorService int $buildingTypeId, int $floorNumber, float $buildingArea, - bool $saveResult = true + bool $saveResult = true, + bool $excelCompatibleMode = false ): array { // Get building type with indices $buildingType = BuildingType::with('indices')->findOrFail($buildingTypeId); @@ -49,7 +50,8 @@ class RetributionCalculatorService $infrastructureMultiplier, $heightMultiplier, $floorNumber, - $buildingArea + $buildingArea, + $excelCompatibleMode ); // Save result if requested @@ -78,7 +80,8 @@ class RetributionCalculatorService float $infrastructureMultiplier, float $heightMultiplier, int $floorNumber, - float $buildingArea + float $buildingArea, + bool $excelCompatibleMode = false ): 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)) @@ -100,7 +103,15 @@ class RetributionCalculatorService $infrastructureCalculation = $infrastructureMultiplier * $mainCalculation; // Step 4: Total retribution (Main + Infrastructure) - $totalRetribution = $mainCalculation + $infrastructureCalculation; + if ($excelCompatibleMode) { + // Try to match Excel exactly - round intermediate calculations + $mainCalculation = round($mainCalculation, 0); + $infrastructureCalculation = round($infrastructureCalculation, 0); + $totalRetribution = $mainCalculation + $infrastructureCalculation; + } else { + // Apply standard rounding to match Excel results more closely + $totalRetribution = round($mainCalculation + $infrastructureCalculation, 0); + } return [ 'building_type' => [ diff --git a/app/Services/RetributionProposalService.php b/app/Services/RetributionProposalService.php deleted file mode 100644 index 45ec828..0000000 --- a/app/Services/RetributionProposalService.php +++ /dev/null @@ -1,294 +0,0 @@ -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; - } -} \ No newline at end of file diff --git a/app/Services/ServiceGoogleSheet.php b/app/Services/ServiceGoogleSheet.php index 68f1752..7389bc2 100644 --- a/app/Services/ServiceGoogleSheet.php +++ b/app/Services/ServiceGoogleSheet.php @@ -6,7 +6,8 @@ use App\Models\BigdataResume; use App\Models\DataSetting; use App\Models\ImportDatasource; use App\Models\PbgTaskGoogleSheet; -use App\Models\RetributionProposal; +use App\Models\SpatialPlanning; +use App\Models\RetributionCalculation; use Carbon\Carbon; use Exception; use Google\Client as Google_Client; @@ -237,7 +238,7 @@ class ServiceGoogleSheet $result = []; foreach ($sections as $key => $identifier) { - $values = $this->get_values_from_section(2, $identifier, [10, 11]); + $values = $this->get_values_from_section($identifier, [10, 11], 2); if (!empty($values)) { $result[$key] = [ @@ -276,8 +277,8 @@ class ServiceGoogleSheet 'process_in_technical_office_count' => $this->convertToInteger($result['PROSES_DINAS_TEKNIS']['total'] ?? null) ?? 0, 'process_in_technical_office_sum' => $this->convertToDecimal($result['PROSES_DINAS_TEKNIS']['nominal'] ?? null) ?? 0, // TATA RUANG - 'spatial_count' => RetributionProposal::count(), - 'spatial_sum' => RetributionProposal::sum('total_retribution_amount'), + 'spatial_count' => $this->getSpatialPlanningWithCalculationCount(), + 'spatial_sum' => $this->getSpatialPlanningCalculationSum() ]); // Save data settings @@ -370,12 +371,12 @@ class ServiceGoogleSheet /** * Get specific values from a row that contains a specific text/section identifier - * @param int $no_sheet Sheet number (0-based) * @param string $section_identifier Text to search for in the row * @param array $column_indices Array of column indices to extract values from + * @param int $no_sheet Sheet number (0-based) * @return array Array of values from specified columns, or empty array if section not found */ - private function get_values_from_section($no_sheet = 1, $section_identifier, $column_indices = []) { + private function get_values_from_section(string $section_identifier, array $column_indices = [], int $no_sheet = 1) { try { $sheet_data = $this->get_data_by_sheet($no_sheet); @@ -469,6 +470,48 @@ class ServiceGoogleSheet return is_numeric($value) ? (float) number_format((float) $value, 2, '.', '') : null; } + /** + * Get count of spatial plannings that have active retribution calculations + */ + public function getSpatialPlanningWithCalculationCount(): int + { + try { + return SpatialPlanning::whereHas('retributionCalculations', function ($query) { + $query->where('is_active', true); + })->count(); + } catch (\Exception $e) { + Log::error("Error getting spatial planning with calculation count", ['error' => $e->getMessage()]); + return 0; + } + } + + /** + * Get total sum of retribution amounts for spatial plannings with active calculations + */ + public function getSpatialPlanningCalculationSum(): float + { + try { + // Get all spatial plannings that have active calculations + $spatialPlannings = SpatialPlanning::whereHas('retributionCalculations', function ($query) { + $query->where('is_active', true); + })->with(['retributionCalculations.retributionCalculation']) + ->get(); + + $totalSum = 0; + foreach ($spatialPlannings as $spatialPlanning) { + $activeCalculation = $spatialPlanning->activeRetributionCalculation; + if ($activeCalculation && $activeCalculation->retributionCalculation) { + $totalSum += $activeCalculation->retributionCalculation->retribution_amount; + } + } + + return (float) $totalSum; + } catch (\Exception $e) { + Log::error("Error getting spatial planning calculation sum", ['error' => $e->getMessage()]); + return 0.0; + } + } + private function convertToDate($dateString) { try { diff --git a/app/Traits/HasRetributionCalculation.php b/app/Traits/HasRetributionCalculation.php new file mode 100644 index 0000000..8fe3ac5 --- /dev/null +++ b/app/Traits/HasRetributionCalculation.php @@ -0,0 +1,79 @@ +morphMany(CalculableRetribution::class, 'calculable'); + } + + /** + * Get active retribution calculation + */ + public function activeRetributionCalculation(): MorphOne + { + return $this->morphOne(CalculableRetribution::class, 'calculable') + ->where('is_active', true) + ->latest('assigned_at'); + } + + /** + * Assign calculation to this model + */ + public function assignRetributionCalculation(RetributionCalculation $calculation, string $notes = null): CalculableRetribution + { + // Deactivate previous active calculation + $this->retributionCalculations() + ->where('is_active', true) + ->update(['is_active' => false]); + + // Create new assignment + return $this->retributionCalculations()->create([ + 'retribution_calculation_id' => $calculation->id, + 'is_active' => true, + 'assigned_at' => now(), + 'notes' => $notes, + ]); + } + + /** + * Get current retribution amount + */ + public function getCurrentRetributionAmount(): float + { + $activeCalculation = $this->activeRetributionCalculation; + + return $activeCalculation + ? $activeCalculation->retributionCalculation->retribution_amount + : 0; + } + + /** + * Check if has active calculation + */ + public function hasActiveRetributionCalculation(): bool + { + return $this->activeRetributionCalculation()->exists(); + } + + /** + * Get calculation history + */ + public function getRetributionCalculationHistory() + { + return $this->retributionCalculations() + ->with('retributionCalculation') + ->orderBy('assigned_at', 'desc') + ->get(); + } +} \ No newline at end of file diff --git a/database/migrations/2025_06_19_103850_create_calculable_retributions_table.php b/database/migrations/2025_06_19_103850_create_calculable_retributions_table.php new file mode 100644 index 0000000..42f83fd --- /dev/null +++ b/database/migrations/2025_06_19_103850_create_calculable_retributions_table.php @@ -0,0 +1,42 @@ +id(); + $table->unsignedBigInteger('retribution_calculation_id'); + $table->morphs('calculable'); // calculable_id & calculable_type (automatically creates index) + $table->boolean('is_active')->default(true)->comment('Status aktif calculation'); + $table->timestamp('assigned_at')->useCurrent()->comment('Kapan calculation di-assign'); + $table->text('notes')->nullable()->comment('Catatan assignment'); + $table->timestamps(); + + // Additional indexes for better performance + $table->index('is_active'); + $table->index('assigned_at'); + + // Foreign key constraint + $table->foreign('retribution_calculation_id') + ->references('id') + ->on('retribution_calculations') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('calculable_retributions'); + } +}; diff --git a/database/seeders/BuildingFunctionParameterSeeder.php b/database/seeders/BuildingFunctionParameterSeeder.php deleted file mode 100644 index 3655d19..0000000 --- a/database/seeders/BuildingFunctionParameterSeeder.php +++ /dev/null @@ -1,148 +0,0 @@ - 'AGAMA', - 'fungsi_bangunan' => 0, - 'ip_permanen' => 0, - 'ip_kompleksitas' => 0, - 'indeks_lokalitas' => 0, - 'asumsi_prasarana' => 0.5, - 'is_active' => true, - 'notes' => 'Parameter untuk fungsi keagamaan' - ], - - // Fungsi Sosial Budaya - [ - 'building_function_code' => 'SOSIAL_BUDAYA', - 'fungsi_bangunan' => 0.3, - 'ip_permanen' => 0.4, - 'ip_kompleksitas' => 0.6, - 'indeks_lokalitas' => 0.3, - 'asumsi_prasarana' => 0.5, - 'is_active' => true, - 'notes' => 'Parameter untuk fungsi sosial budaya' - ], - - // Fungsi Campuran Kecil - [ - 'building_function_code' => 'CAMPURAN_KECIL', - 'fungsi_bangunan' => 0.6, - 'ip_permanen' => 0.4, - 'ip_kompleksitas' => 0.6, - 'indeks_lokalitas' => 0.5, - 'asumsi_prasarana' => 0.5, - 'is_active' => true, - 'notes' => 'Parameter untuk fungsi campuran kecil' - ], - - // Fungsi Campuran Besar - [ - 'building_function_code' => 'CAMPURAN_BESAR', - 'fungsi_bangunan' => 0.8, - 'ip_permanen' => 0.4, - 'ip_kompleksitas' => 0.6, - 'indeks_lokalitas' => 0.5, - 'asumsi_prasarana' => 0.5, - 'is_active' => true, - 'notes' => 'Parameter untuk fungsi campuran besar' - ], - - // UMKM (Usaha Kecil) - [ - 'building_function_code' => 'USAHA_KECIL', - 'fungsi_bangunan' => 0.5, - 'ip_permanen' => 0.4, - 'ip_kompleksitas' => 0.6, - 'indeks_lokalitas' => 0.4, - 'asumsi_prasarana' => 0.5, - 'is_active' => true, - 'notes' => 'Parameter untuk UMKM' - ], - - // Usaha Besar (Non-Mikro) - [ - 'building_function_code' => 'USAHA_BESAR', - 'fungsi_bangunan' => 0.7, - 'ip_permanen' => 0.4, - 'ip_kompleksitas' => 0.6, - 'indeks_lokalitas' => 0.5, - 'asumsi_prasarana' => 0.5, - 'is_active' => true, - 'notes' => 'Parameter untuk usaha besar (non-mikro)' - ], - - // Hunian Sederhana < 100 m2 - [ - 'building_function_code' => 'HUNIAN_KECIL', - 'fungsi_bangunan' => 0.15, - 'ip_permanen' => 0.4, - 'ip_kompleksitas' => 0.3, - 'indeks_lokalitas' => 0.4, - 'asumsi_prasarana' => 0.5, - 'is_active' => true, - 'notes' => 'Parameter untuk hunian sederhana < 100 m2' - ], - - // Hunian Sederhana > 100 m2 - [ - 'building_function_code' => 'HUNIAN_BESAR', - 'fungsi_bangunan' => 0.17, - 'ip_permanen' => 0.4, - 'ip_kompleksitas' => 0.6, - 'indeks_lokalitas' => 0.4, - 'asumsi_prasarana' => 0.5, - 'is_active' => true, - 'notes' => 'Parameter untuk hunian sederhana > 100 m2' - ], - - // MBR (Masyarakat Berpenghasilan Rendah) - [ - 'building_function_code' => 'HUNIAN_MBR', - 'fungsi_bangunan' => 0, - 'ip_permanen' => 0, - 'ip_kompleksitas' => 0, - 'indeks_lokalitas' => 0, - 'asumsi_prasarana' => 0.5, - 'is_active' => true, - 'notes' => 'Parameter untuk MBR (tarif lebih rendah)' - ] - ]; - - foreach ($parameters as $parameterData) { - $buildingFunction = BuildingFunction::where('code', $parameterData['building_function_code'])->first(); - - if ($buildingFunction) { - // Remove building_function_code from parameter data - $parameterData['building_function_id'] = $buildingFunction->id; - unset($parameterData['building_function_code']); - - BuildingFunctionParameter::updateOrCreate( - ['building_function_id' => $buildingFunction->id], - $parameterData - ); - - $this->command->info("Created/Updated parameter for: {$buildingFunction->name}"); - } else { - $this->command->warn("Building function not found: {$parameterData['building_function_code']}"); - } - } - - $this->command->info('Building Function Parameters seeding completed!'); - } -} diff --git a/database/seeders/BuildingFunctionSeeder.php b/database/seeders/BuildingFunctionSeeder.php deleted file mode 100644 index 705a6a8..0000000 --- a/database/seeders/BuildingFunctionSeeder.php +++ /dev/null @@ -1,156 +0,0 @@ - 1, 'code' => 'AGAMA', 'name' => 'Bangunan Keagamaan', 'parent_id' => null, 'level' => 0, 'sort_order' => 1, 'base_tariff' => 0.00], - ['id' => 2, 'code' => 'SOSIAL_BUDAYA', 'name' => 'Bangunan Sosial Budaya', 'parent_id' => null, 'level' => 0, 'sort_order' => 2, 'base_tariff' => 30000.00], - ['id' => 3, 'code' => 'CAMPURAN', 'name' => 'Bangunan Campuran', 'parent_id' => null, 'level' => 0, 'sort_order' => 3, 'base_tariff' => 70000.00], - ['id' => 4, 'code' => 'USAHA', 'name' => 'Bangunan Usaha', 'parent_id' => null, 'level' => 0, 'sort_order' => 4, 'base_tariff' => 60000.00], - ['id' => 5, 'code' => 'HUNIAN', 'name' => 'Bangunan Hunian', 'parent_id' => null, 'level' => 0, 'sort_order' => 5, 'base_tariff' => 40000.00], - - // Sub Categories - Campuran - ['id' => 6, 'code' => 'CAMPURAN_KECIL', 'name' => 'Bangunan Campuran Kecil', 'parent_id' => 3, 'level' => 1, 'sort_order' => 1, 'base_tariff' => 60000.00], - ['id' => 7, 'code' => 'CAMPURAN_BESAR', 'name' => 'Bangunan Campuran Besar', 'parent_id' => 3, 'level' => 1, 'sort_order' => 2, 'base_tariff' => 80000.00], - - // Sub Categories - Usaha - ['id' => 8, 'code' => 'USAHA_KECIL', 'name' => 'Bangunan Usaha Kecil (UMKM)', 'parent_id' => 4, 'level' => 1, 'sort_order' => 1, 'base_tariff' => 50000.00], - ['id' => 9, 'code' => 'USAHA_BESAR', 'name' => 'Bangunan Usaha Besar', 'parent_id' => 4, 'level' => 1, 'sort_order' => 2, 'base_tariff' => 70000.00], - - // Sub Categories - Hunian - ['id' => 10, 'code' => 'HUNIAN_SEDERHANA', 'name' => 'Hunian Sederhana', 'parent_id' => 5, 'level' => 1, 'sort_order' => 1, 'base_tariff' => 35000.00], - ['id' => 11, 'code' => 'HUNIAN_TIDAK_SEDERHANA', 'name' => 'Hunian Tidak Sederhana', 'parent_id' => 5, 'level' => 1, 'sort_order' => 2, 'base_tariff' => 45000.00], - ['id' => 12, 'code' => 'MBR', 'name' => 'Hunian MBR (Masyarakat Berpenghasilan Rendah)', 'parent_id' => 5, 'level' => 1, 'sort_order' => 3, 'base_tariff' => 0.00], - ]; - - foreach ($buildingFunctions as $function) { - DB::table('building_functions')->updateOrInsert( - ['id' => $function['id']], - array_merge($function, [ - 'description' => 'Deskripsi untuk ' . $function['name'], - 'created_at' => $now, - 'updated_at' => $now - ]) - ); - } - - // 2. Building Function Parameters - $parameters = [ - // AGAMA - No charge - ['building_function_id' => 1, 'fungsi_bangunan' => 0, 'ip_permanen' => 0, 'ip_kompleksitas' => 0, 'indeks_lokalitas' => 0, 'asumsi_prasarana' => 0.5], - - // SOSIAL_BUDAYA - ['building_function_id' => 2, 'fungsi_bangunan' => 0.3, 'ip_permanen' => 0.4, 'ip_kompleksitas' => 0.6, 'indeks_lokalitas' => 0.3, 'asumsi_prasarana' => 0.5], - - // CAMPURAN_KECIL - ['building_function_id' => 6, 'fungsi_bangunan' => 0.6, 'ip_permanen' => 0.4, 'ip_kompleksitas' => 0.6, 'indeks_lokalitas' => 0.5, 'asumsi_prasarana' => 0.5], - - // CAMPURAN_BESAR - ['building_function_id' => 7, 'fungsi_bangunan' => 0.8, 'ip_permanen' => 0.4, 'ip_kompleksitas' => 0.6, 'indeks_lokalitas' => 0.5, 'asumsi_prasarana' => 0.5], - - // USAHA_KECIL - ['building_function_id' => 8, 'fungsi_bangunan' => 0.5, 'ip_permanen' => 0.4, 'ip_kompleksitas' => 0.6, 'indeks_lokalitas' => 0.4, 'asumsi_prasarana' => 0.5], - - // USAHA_BESAR - ['building_function_id' => 9, 'fungsi_bangunan' => 0.7, 'ip_permanen' => 0.4, 'ip_kompleksitas' => 0.6, 'indeks_lokalitas' => 0.5, 'asumsi_prasarana' => 0.5], - - // HUNIAN_SEDERHANA - ['building_function_id' => 10, 'fungsi_bangunan' => 0.15, 'ip_permanen' => 0.4, 'ip_kompleksitas' => 0.3, 'indeks_lokalitas' => 0.4, 'asumsi_prasarana' => 0.5], - - // HUNIAN_TIDAK_SEDERHANA - ['building_function_id' => 11, 'fungsi_bangunan' => 0.17, 'ip_permanen' => 0.4, 'ip_kompleksitas' => 0.6, 'indeks_lokalitas' => 0.4, 'asumsi_prasarana' => 0.5], - - // MBR - No charge - ['building_function_id' => 12, 'fungsi_bangunan' => 0, 'ip_permanen' => 0, 'ip_kompleksitas' => 0, 'indeks_lokalitas' => 0, 'asumsi_prasarana' => 0.5], - ]; - - foreach ($parameters as $param) { - DB::table('building_function_parameters')->updateOrInsert( - ['building_function_id' => $param['building_function_id']], - array_merge($param, [ - 'koefisien_dasar' => 1.0, - 'created_at' => $now, - 'updated_at' => $now - ]) - ); - } - - // 3. Floor Height Indices (IP Ketinggian per lantai) - $floorHeightIndices = [ - ['floor_number' => 1, 'ip_ketinggian' => 1.000000, 'description' => 'IP ketinggian untuk lantai 1'], - ['floor_number' => 2, 'ip_ketinggian' => 1.090000, 'description' => 'IP ketinggian untuk lantai 2'], - ['floor_number' => 3, 'ip_ketinggian' => 1.120000, 'description' => 'IP ketinggian untuk lantai 3'], - ['floor_number' => 4, 'ip_ketinggian' => 1.350000, 'description' => 'IP ketinggian untuk lantai 4'], - ['floor_number' => 5, 'ip_ketinggian' => 1.620000, 'description' => 'IP ketinggian untuk lantai 5'], - ['floor_number' => 6, 'ip_ketinggian' => 1.197000, 'description' => 'IP ketinggian untuk lantai 6'], - ]; - - foreach ($floorHeightIndices as $heightIndex) { - DB::table('floor_height_indices')->updateOrInsert( - ['floor_number' => $heightIndex['floor_number']], - array_merge($heightIndex, [ - 'created_at' => $now, - 'updated_at' => $now - ]) - ); - } - - // Formula Templates removed - not used in current system - - // 5. Retribution Formulas per Floor (for child building functions only) - $childBuildingFunctionIds = [1, 2, 6, 7, 8, 9, 10, 11, 12]; // All leaf nodes - $formulaExpression = '(1 * luas_bangunan * (indeks_lokalitas * 70350 * ((fungsi_bangunan * (ip_permanen + ip_kompleksitas + (0.5 * ip_ketinggian)))) * 1)) + (0.5 * (1 * luas_bangunan * (indeks_lokalitas * 70350 * ((fungsi_bangunan * (ip_permanen + ip_kompleksitas + (0.5 * ip_ketinggian)))) * 1)))'; - - foreach ($childBuildingFunctionIds as $buildingFunctionId) { - $buildingFunction = DB::table('building_functions')->where('id', $buildingFunctionId)->first(); - - if ($buildingFunction) { - // Create formulas for floors 1-5 - for ($floorNumber = 1; $floorNumber <= 5; $floorNumber++) { - DB::table('retribution_formulas')->updateOrInsert( - [ - 'building_function_id' => $buildingFunctionId, - 'floor_number' => $floorNumber - ], - [ - 'name' => "{$buildingFunction->name} - Lantai {$floorNumber}", - 'formula_expression' => $formulaExpression, - 'created_at' => $now, - 'updated_at' => $now - ] - ); - } - } - } - - $this->command->info('Building Function data seeded successfully!'); - $this->command->info('Building Function Parameters seeded successfully!'); - $this->command->info('Floor Height Indices seeded successfully!'); - $this->command->info('Retribution Formulas with IP Ketinggian seeded successfully!'); - - // Display summary - $this->command->info('=== SUMMARY ==='); - $this->command->info('Parent Functions: ' . DB::table('building_functions')->whereNull('parent_id')->count()); - $this->command->info('Child Functions: ' . DB::table('building_functions')->whereNotNull('parent_id')->count()); - $this->command->info('Parameters: ' . DB::table('building_function_parameters')->count()); - $this->command->info('Floor Height Indices: ' . DB::table('floor_height_indices')->count()); - $this->command->info('Retribution Formulas: ' . DB::table('retribution_formulas')->count()); - } -} \ No newline at end of file diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 9bdc969..9e464d4 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -50,12 +50,7 @@ class DatabaseSeeder extends Seeder MenuSeeder::class, UsersRoleMenuSeeder::class, GlobalSettingSeeder::class, - BuildingFunctionSeeder::class, - BuildingFunctionParameterSeeder::class, - FloorHeightIndexSeeder::class, - RetributionFormulaSeeder::class, - RetributionProposalSeeder::class, - RetributionDataSeeder::class, // New optimized retribution data + RetributionDataSeeder::class, ]); } } diff --git a/database/seeders/FloorHeightIndexSeeder.php b/database/seeders/FloorHeightIndexSeeder.php deleted file mode 100644 index c668ddb..0000000 --- a/database/seeders/FloorHeightIndexSeeder.php +++ /dev/null @@ -1,62 +0,0 @@ - 1, - 'ip_ketinggian' => 1.000000, - 'description' => 'IP ketinggian untuk lantai 1' - ], - [ - 'floor_number' => 2, - 'ip_ketinggian' => 1.090000, - 'description' => 'IP ketinggian untuk lantai 2' - ], - [ - 'floor_number' => 3, - 'ip_ketinggian' => 1.120000, - 'description' => 'IP ketinggian untuk lantai 3' - ], - [ - 'floor_number' => 4, - 'ip_ketinggian' => 1.350000, - 'description' => 'IP ketinggian untuk lantai 4' - ], - [ - 'floor_number' => 5, - 'ip_ketinggian' => 1.620000, - 'description' => 'IP ketinggian untuk lantai 5' - ], - [ - 'floor_number' => 6, - 'ip_ketinggian' => 1.197000, - 'description' => 'IP ketinggian untuk lantai 6' - ] - ]; - - foreach ($indices as $index) { - FloorHeightIndex::updateOrCreate( - ['floor_number' => $index['floor_number']], - $index - ); - } - - $this->command->info('Floor Height Index seeding completed!'); - $this->command->info('IP Ketinggian values:'); - foreach ($indices as $index) { - $this->command->info("Lantai {$index['floor_number']}: {$index['ip_ketinggian']}"); - } - } -} diff --git a/database/seeders/RetributionFormulaSeeder.php b/database/seeders/RetributionFormulaSeeder.php deleted file mode 100644 index b442a00..0000000 --- a/database/seeders/RetributionFormulaSeeder.php +++ /dev/null @@ -1,74 +0,0 @@ -first(); - - if ($buildingFunction) { - $formulaName = "{$buildingFunction->name} - Lantai {$floorNumber}"; - - RetributionFormula::updateOrCreate( - [ - 'building_function_id' => $buildingFunction->id, - 'floor_number' => $floorNumber - ], - [ - 'name' => $formulaName, - 'formula_expression' => $fullFormulaExpression, - 'description' => "Formula retribusi untuk {$buildingFunction->name} dengan {$floorNumber} lantai. Formula: {$fullFormulaExpression}", - 'floor_number' => $floorNumber, - 'luas_bangunan_rate' => $buildingFunction->base_tariff ?? 50000, - 'is_active' => true - ] - ); - - $this->command->info("Created formula for: {$formulaName}"); - } else { - $this->command->warn("Building function not found: {$code}"); - } - } - } - - $this->command->info('Retribution Formulas seeding completed for 5 floors!'); - $this->command->info("Building functions processed: " . implode(', ', $buildingFunctionCodes)); - $this->command->info("Floor calculation formula: {$floorCalculationFormula}"); - $this->command->info("Full retribution formula: {$fullFormulaExpression}"); - } -} diff --git a/database/seeders/RetributionProposalSeeder.php b/database/seeders/RetributionProposalSeeder.php deleted file mode 100644 index 7a7d7d0..0000000 --- a/database/seeders/RetributionProposalSeeder.php +++ /dev/null @@ -1,159 +0,0 @@ -get(); - $buildingFunctions = BuildingFunction::whereNotNull('parent_id')->get(); // Only child functions - - if ($spatialPlannings->isEmpty() || $buildingFunctions->isEmpty()) { - $this->command->warn('No spatial plannings or building functions found. Please seed them first.'); - return; - } - - $sampleProposals = [ - [ - 'spatial_planning_id' => $spatialPlannings->first()?->id, - 'building_function_code' => 'HUNIAN_SEDERHANA', - 'floor_number' => 2, - 'floor_area' => 45666, - 'total_building_area' => 91332, // 2 floors - 'notes' => 'Sample calculation for Hunian Sederhana' - ], - [ - 'spatial_planning_id' => $spatialPlannings->skip(1)->first()?->id, - 'building_function_code' => 'USAHA_KECIL', - 'floor_number' => 1, - 'floor_area' => 150, - 'total_building_area' => 150, - 'notes' => 'Sample calculation for UMKM' - ], - [ - 'spatial_planning_id' => null, // Testing nullable spatial_planning_id - 'building_function_code' => 'CAMPURAN_KECIL', - 'floor_number' => 3, - 'floor_area' => 200, - 'total_building_area' => 600, - 'notes' => 'Sample calculation without spatial planning link' - ] - ]; - - foreach ($sampleProposals as $proposalData) { - $buildingFunction = BuildingFunction::where('code', $proposalData['building_function_code'])->first(); - - if (!$buildingFunction) { - $this->command->warn("Building function not found: {$proposalData['building_function_code']}"); - continue; - } - - // Get parameters - $parameters = BuildingFunctionParameter::where('building_function_id', $buildingFunction->id)->first(); - $floorHeightIndex = FloorHeightIndex::where('floor_number', $proposalData['floor_number'])->first(); - $retributionFormula = RetributionFormula::where('building_function_id', $buildingFunction->id) - ->where('floor_number', $proposalData['floor_number']) - ->first(); - - if (!$parameters || !$floorHeightIndex || !$retributionFormula) { - $this->command->warn("Missing data for building function: {$buildingFunction->name}"); - continue; - } - - // Calculate retribution using the Excel formula - $floorArea = $proposalData['floor_area']; - $fungsi_bangunan = $parameters->fungsi_bangunan; - $ip_permanen = $parameters->ip_permanen; - $ip_kompleksitas = $parameters->ip_kompleksitas; - $ip_ketinggian = $floorHeightIndex->ip_ketinggian; - $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 * $ip_ketinggian)); - - // 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; - - // Prepare calculation parameters and breakdown - $calculationParameters = [ - 'fungsi_bangunan' => $fungsi_bangunan, - 'ip_permanen' => $ip_permanen, - 'ip_kompleksitas' => $ip_kompleksitas, - 'ip_ketinggian' => $ip_ketinggian, - 'indeks_lokalitas' => $indeks_lokalitas, - 'base_value' => $base_value, - 'additional_factor' => $additional_factor, - 'floor_area' => $floorArea - ]; - - $calculationBreakdown = [ - 'h13_calculation' => [ - 'formula' => 'fungsi_bangunan * (ip_permanen + ip_kompleksitas + (0.5 * ip_ketinggian))', - 'calculation' => "{$fungsi_bangunan} * ({$ip_permanen} + {$ip_kompleksitas} + (0.5 * {$ip_ketinggian}))", - '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 - ] - ]; - - // Create the proposal - RetributionProposal::create([ - 'spatial_planning_id' => $proposalData['spatial_planning_id'], - 'building_function_id' => $buildingFunction->id, - 'retribution_formula_id' => $retributionFormula->id, - 'floor_number' => $proposalData['floor_number'], - 'floor_area' => $proposalData['floor_area'], - 'total_building_area' => $proposalData['total_building_area'], - 'ip_ketinggian' => $ip_ketinggian, - 'floor_retribution_amount' => $total_retribution, - 'total_retribution_amount' => $total_retribution, // For single floor, same as floor amount - 'calculation_parameters' => $calculationParameters, - 'calculation_breakdown' => $calculationBreakdown, - 'notes' => $proposalData['notes'], - 'calculated_at' => Carbon::now() - ]); - - $this->command->info("Created retribution proposal for: {$buildingFunction->name} - Floor {$proposalData['floor_number']}"); - $this->command->info(" Amount: Rp " . number_format($total_retribution, 0, ',', '.')); - } - - $this->command->info('Retribution Proposal seeding completed!'); - $this->command->info('Total proposals created: ' . RetributionProposal::count()); - } -} \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 73a0d2d..3c1ca88 100644 --- a/routes/api.php +++ b/routes/api.php @@ -28,7 +28,7 @@ use App\Http\Controllers\Api\UmkmController; use App\Http\Controllers\Api\TourismController; use App\Http\Controllers\Api\SpatialPlanningController; use App\Http\Controllers\Api\ChatbotController; -use App\Http\Controllers\RetributionProposalController; + use Illuminate\Support\Facades\Route; Route::post('/login', [UsersController::class, 'login'])->name('api.user.login'); @@ -194,16 +194,5 @@ Route::group(['middleware' => 'auth:sanctum'], function (){ Route::get('/growth','index')->name('api.growth'); }); - // Retribution Proposal API - Route::controller(RetributionProposalController::class)->group(function(){ - Route::get('/retribution-proposals', 'index')->name('api.retribution-proposals.index'); - Route::post('/retribution-proposals', 'store')->name('api.retribution-proposals.store'); - Route::get('/retribution-proposals/statistics', 'statistics')->name('api.retribution-proposals.statistics'); - Route::get('/retribution-proposals/total-sum', 'totalSum')->name('api.retribution-proposals.total-sum'); - Route::get('/retribution-proposals/building-functions', 'buildingFunctions')->name('api.retribution-proposals.building-functions'); - Route::post('/retribution-proposals/auto-create', 'autoCreateProposal')->name('api.retribution-proposals.auto-create'); - Route::get('/retribution-proposals/{id}', 'show')->name('api.retribution-proposals.show'); - Route::put('/retribution-proposals/{id}', 'update')->name('api.retribution-proposals.update'); - Route::delete('/retribution-proposals/{id}', 'destroy')->name('api.retribution-proposals.destroy'); - }); + // TODO: Implement new retribution calculation API endpoints using the new schema }); \ No newline at end of file