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