add spatial plannings retribution calculations

This commit is contained in:
arifal hidayat
2025-06-18 02:54:41 +07:00
parent 6946fa7074
commit fc54e20fa4
29 changed files with 2926 additions and 416 deletions

View File

@@ -0,0 +1,294 @@
<?php
namespace App\Services;
use App\Models\SpatialPlanning;
use App\Models\BuildingFunction;
use App\Models\RetributionProposal;
use App\Models\RetributionFormula;
use App\Models\FloorHeightIndex;
use App\Models\BuildingFunctionParameter;
use Carbon\Carbon;
class RetributionProposalService
{
/**
* Create retribution proposal for spatial planning
*/
public function createProposalForSpatialPlanning(
SpatialPlanning $spatialPlanning,
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' => $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;
}
}