diff --git a/app/Console/Commands/AssignSpatialPlanningsToCalculation.php b/app/Console/Commands/AssignSpatialPlanningsToCalculation.php index e066d1b..3214a63 100644 --- a/app/Console/Commands/AssignSpatialPlanningsToCalculation.php +++ b/app/Console/Commands/AssignSpatialPlanningsToCalculation.php @@ -26,7 +26,7 @@ class AssignSpatialPlanningsToCalculation extends Command * * @var string */ - protected $description = 'Assign retribution calculations to spatial plannings (supports recalculate for existing calculations)'; + protected $description = 'Assign retribution calculations to spatial plannings (recalculate mode applies 30% area adjustment)'; protected $calculatorService; @@ -57,6 +57,7 @@ class AssignSpatialPlanningsToCalculation extends Command $q->where('is_active', true); }); $this->info('🔄 Recalculate mode: Processing spatial plannings with existing calculations'); + $this->warn('⚠️ NOTE: Recalculate mode will apply 30% area adjustment to all calculations'); } elseif (!$force) { // Normal mode: only process those without active calculations $query->whereDoesntHave('retributionCalculations', function ($q) { @@ -140,6 +141,7 @@ class AssignSpatialPlanningsToCalculation extends Command ['Errors', $errors], ] ); + $this->info('📊 Recalculate mode applied 30% area adjustment to all calculations'); } else { $this->table( ['Metric', 'Count'], @@ -189,7 +191,7 @@ class AssignSpatialPlanningsToCalculation extends Command if ($recalculate) { // Recalculate mode: Always create new calculation - $calculationResult = $this->performCalculation($spatialPlanning, $buildingType); + $calculationResult = $this->performCalculation($spatialPlanning, $buildingType, true); // Check if spatial planning has existing active calculation $currentActiveCalculation = $spatialPlanning->activeRetributionCalculation; @@ -211,9 +213,10 @@ class AssignSpatialPlanningsToCalculation extends Command ]); // Assign new calculation + $adjustedArea = round($buildingArea * 0.3, 2); $spatialPlanning->assignRetributionCalculation( $calculation, - "Recalculated: Area {$oldArea}→{$buildingArea}, Amount {$oldAmount}→{$newAmount}" + "Recalculated (30% area): Original area {$oldArea}m² → Adjusted area {$adjustedArea}m², Amount {$oldAmount}→{$newAmount}" ); $isRecalculated = true; @@ -234,7 +237,7 @@ class AssignSpatialPlanningsToCalculation extends Command $spatialPlanning->assignRetributionCalculation( $calculation, - 'Recalculated (new calculation)' + 'Recalculated (new calculation with 30% area adjustment)' ); } } else { @@ -255,7 +258,7 @@ class AssignSpatialPlanningsToCalculation extends Command $reused = true; } else { // Create new calculation - $calculationResult = $this->performCalculation($spatialPlanning, $buildingType); + $calculationResult = $this->performCalculation($spatialPlanning, $buildingType, false); $calculation = RetributionCalculation::create([ 'building_type_id' => $buildingType->id, @@ -401,10 +404,18 @@ class AssignSpatialPlanningsToCalculation extends Command /** * Perform calculation using RetributionCalculatorService */ - private function performCalculation(SpatialPlanning $spatialPlanning, BuildingType $buildingType): array + private function performCalculation(SpatialPlanning $spatialPlanning, BuildingType $buildingType, bool $recalculate = false): array { // Round area to 2 decimal places to match database storage format $buildingArea = round($spatialPlanning->getCalculationArea(), 2); + + // Apply 30% multiplication for recalculate mode + if ($recalculate) { + $originalArea = $buildingArea; + $buildingArea = round($buildingArea * 0.3, 2); // 30% of original area + $this->info("Recalculate mode: Original area {$originalArea}m² → Adjusted area {$buildingArea}m² (30%)"); + } + $floorNumber = $spatialPlanning->number_of_floors ?: 1; try { @@ -429,6 +440,8 @@ class AssignSpatialPlanningsToCalculation extends Command 'height_index' => $result['input_parameters']['height_index'], 'infrastructure_factor' => $result['indices']['infrastructure_factor'], 'building_area' => $buildingArea, + 'original_building_area' => $recalculate ? round($spatialPlanning->getCalculationArea(), 2) : null, + 'area_adjustment_factor' => $recalculate ? 0.3 : null, 'floor_number' => $floorNumber, 'building_function' => $spatialPlanning->building_function, 'calculation_steps' => $result['calculation_detail'], @@ -436,6 +449,7 @@ class AssignSpatialPlanningsToCalculation extends Command 'is_free' => $buildingType->is_free, 'calculation_date' => now()->toDateTimeString(), 'total' => $result['total_retribution'], + 'is_recalculated' => $recalculate, ] ]; @@ -446,6 +460,13 @@ class AssignSpatialPlanningsToCalculation extends Command // Basic fallback calculation $totalAmount = $buildingType->is_free ? 0 : ($buildingArea * 50000); + // Apply 30% multiplication for recalculate mode in fallback too + if ($recalculate) { + $originalAmount = $totalAmount; + $totalAmount = round($totalAmount * 0.3, 2); + $this->warn("Fallback recalculate: Original amount Rp{$originalAmount} → Adjusted amount Rp{$totalAmount} (30%)"); + } + return [ 'amount' => $totalAmount, 'detail' => [ @@ -453,6 +474,8 @@ class AssignSpatialPlanningsToCalculation extends Command 'building_type_name' => $buildingType->name, 'building_type_code' => $buildingType->code, 'building_area' => $buildingArea, + 'original_building_area' => $recalculate ? round($spatialPlanning->getCalculationArea(), 2) : null, + 'area_adjustment_factor' => $recalculate ? 0.3 : null, 'floor_number' => $floorNumber, 'building_function' => $spatialPlanning->building_function, 'calculation_method' => 'fallback', @@ -460,6 +483,7 @@ class AssignSpatialPlanningsToCalculation extends Command 'is_free' => $buildingType->is_free, 'calculation_date' => now()->toDateTimeString(), 'total' => $totalAmount, + 'is_recalculated' => $recalculate, ] ]; } diff --git a/app/Console/Commands/InjectSpatialPlanningsData.php b/app/Console/Commands/InjectSpatialPlanningsData.php index 7ef29da..d0c73c6 100644 --- a/app/Console/Commands/InjectSpatialPlanningsData.php +++ b/app/Console/Commands/InjectSpatialPlanningsData.php @@ -20,7 +20,9 @@ class InjectSpatialPlanningsData extends Command {--file=storage/app/public/templates/2025.xlsx : Path to Excel file} {--sheet=0 : Sheet index to read from} {--dry-run : Run without actually inserting data} - {--debug : Show Excel content for debugging}'; + {--debug : Show Excel content for debugging} + {--truncate : Clear existing data before import} + {--no-truncate : Skip truncation (keep existing data)}'; /** * The console command description. @@ -39,6 +41,8 @@ class InjectSpatialPlanningsData extends Command $sheetIndex = (int) $this->option('sheet'); $isDryRun = $this->option('dry-run'); $isDebug = $this->option('debug'); + $shouldTruncate = $this->option('truncate'); + $noTruncate = $this->option('no-truncate'); if (!file_exists($filePath)) { $this->error("File not found: {$filePath}"); @@ -52,10 +56,86 @@ class InjectSpatialPlanningsData extends Command $this->warn("DRY RUN MODE - No data will be inserted"); } - DB::statement('SET FOREIGN_KEY_CHECKS = 0'); - DB::table('spatial_plannings')->truncate(); - DB::statement('SET FOREIGN_KEY_CHECKS = 1'); - $this->info('Spatial plannings table truncated successfully.'); + // Check existing data + $existingCount = DB::table('spatial_plannings')->count(); + if ($existingCount > 0) { + $this->info("Found {$existingCount} existing spatial planning records"); + } else { + $this->info('No existing spatial planning data found'); + } + + // Handle truncation logic + $willTruncate = false; + + if ($shouldTruncate) { + $willTruncate = true; + $this->info('Truncation requested via --truncate option'); + } elseif ($noTruncate) { + $willTruncate = false; + $this->info('Truncation skipped via --no-truncate option'); + } else { + // Default behavior: ask user if not in dry run mode + if (!$isDryRun) { + $willTruncate = $this->confirm('Do you want to clear existing spatial planning data before import?'); + } else { + $willTruncate = false; + $this->info('DRY RUN MODE - Truncation will be skipped'); + } + } + + // Confirm truncation if not in dry run mode and truncation is requested + if ($willTruncate && !$isDryRun) { + if (!$this->confirm('This will delete all existing spatial planning data and related retribution calculations. Continue?')) { + $this->info('Operation cancelled.'); + return 0; + } + } + + // Truncate all related data properly + if ($willTruncate && !$isDryRun) { + $this->info('Truncating spatial planning data and related retribution calculations...'); + + try { + // Disable foreign key checks for safe truncation + DB::statement('SET FOREIGN_KEY_CHECKS = 0'); + + // 1. Delete calculable retributions for spatial plannings (polymorphic relationship) + $deletedCalculableRetributions = DB::table('calculable_retributions') + ->where('calculable_type', 'App\\Models\\SpatialPlanning') + ->count(); + + if ($deletedCalculableRetributions > 0) { + DB::table('calculable_retributions') + ->where('calculable_type', 'App\\Models\\SpatialPlanning') + ->delete(); + $this->info("Deleted {$deletedCalculableRetributions} calculable retributions for spatial plannings."); + } + + // 2. Truncate spatial plannings table + DB::table('spatial_plannings')->truncate(); + $this->info('Spatial plannings table truncated successfully.'); + + // Re-enable foreign key checks + DB::statement('SET FOREIGN_KEY_CHECKS = 1'); + + $this->info('All spatial planning data and related retribution calculations cleared successfully.'); + + } catch (Exception $e) { + // Make sure to re-enable foreign key checks even on error + try { + DB::statement('SET FOREIGN_KEY_CHECKS = 1'); + } catch (Exception $fkError) { + $this->error('Failed to re-enable foreign key checks: ' . $fkError->getMessage()); + } + + $this->error('Failed to truncate spatial planning data: ' . $e->getMessage()); + return 1; + } + } elseif ($willTruncate && $isDryRun) { + $this->info('DRY RUN MODE - Would truncate spatial planning data and related retribution calculations'); + } else { + $this->info('Keeping existing data (no truncation)'); + } $spreadsheet = IOFactory::load($filePath); $worksheet = $spreadsheet->getSheet($sheetIndex); @@ -97,8 +177,23 @@ class InjectSpatialPlanningsData extends Command if (!$isDryRun) { $this->info("Successfully inserted {$totalInserted} spatial planning records"); + + // Show summary of what was done + $finalCount = DB::table('spatial_plannings')->count(); + $this->info("Final spatial planning records count: {$finalCount}"); + + if ($willTruncate) { + $this->info("✅ Data import completed with truncation"); + } else { + $this->info("✅ Data import completed (existing data preserved)"); + } } else { $this->info("Dry run completed. Total records that would be inserted: " . count($sections)); + if ($willTruncate) { + $this->info("Would truncate existing data before import"); + } else { + $this->info("Would preserve existing data during import"); + } } return 0; @@ -574,8 +669,6 @@ class InjectSpatialPlanningsData extends Command strpos($activitiesLower, 'perdaganagan') !== false || strpos($activitiesLower, 'waterpark') !== false || strpos($activitiesLower, 'pasar') !== false || - strpos($activitiesLower, 'perumahan') !== false || - strpos($activitiesLower, 'perumhan') !== false || strpos($activitiesLower, 'kantor') !== false) { $buildingFunction = 'Fungsi Usaha'; @@ -601,6 +694,8 @@ class InjectSpatialPlanningsData extends Command // Determine housing type based on area and keywords if (strpos($activitiesLower, 'mbr') !== false || strpos($activitiesLower, 'masyarakat berpenghasilan rendah') !== false || + strpos($activitiesLower, 'perumahan') !== false || + strpos($activitiesLower, 'perumhan') !== false || strpos($activitiesLower, 'sederhana') !== false || ($landArea > 0 && $landArea < 2000)) { // Small area indicates MBR diff --git a/app/Services/ServiceGoogleSheet.php b/app/Services/ServiceGoogleSheet.php index 12a8a38..8df16c7 100644 --- a/app/Services/ServiceGoogleSheet.php +++ b/app/Services/ServiceGoogleSheet.php @@ -537,7 +537,7 @@ class ServiceGoogleSheet foreach ($spatialPlannings as $spatialPlanning) { $activeCalculation = $spatialPlanning->activeRetributionCalculation; if ($activeCalculation && $activeCalculation->retributionCalculation) { - $totalSum += $activeCalculation->retributionCalculation->retribution_amount; + $totalSum += $activeCalculation->retributionCalculation->retribution_amount * 0.3; } } diff --git a/database/seeders/RetributionDataSeeder.php b/database/seeders/RetributionDataSeeder.php index c80b5b9..4e1bcc6 100644 --- a/database/seeders/RetributionDataSeeder.php +++ b/database/seeders/RetributionDataSeeder.php @@ -29,7 +29,7 @@ class RetributionDataSeeder extends Seeder ['id' => 9, 'code' => 'USH_BESAR', 'name' => 'Usaha Besar (Non-Mikro)', 'parent_id' => 4, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], ['id' => 10, 'code' => 'HUN_SEDH', 'name' => 'Hunian Sederhana <100', 'parent_id' => 5, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], ['id' => 11, 'code' => 'HUN_TSEDH', 'name' => 'Hunian Tidak Sederhana >100', 'parent_id' => 5, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], - ['id' => 12, 'code' => 'MBR', 'name' => 'Rumah Tinggal MBR', 'parent_id' => 5, 'level' => 2, 'is_free' => true, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['id' => 12, 'code' => 'MBR', 'name' => 'Rumah Tinggal MBR', 'parent_id' => 5, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], ]); // Seed Retribution Indices berdasarkan Excel (with coefficient moved here)