add spatial plannings retribution calculations
This commit is contained in:
294
app/Services/RetributionProposalService.php
Normal file
294
app/Services/RetributionProposalService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user