288 lines
9.2 KiB
PHP
288 lines
9.2 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use App\Models\SpatialPlanning;
|
|
use App\Models\RetributionProposal;
|
|
use App\Services\RetributionProposalService;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Exception;
|
|
|
|
class CalculateRetributionProposalsCommand extends Command
|
|
{
|
|
/**
|
|
* The name and signature of the console command.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $signature = 'retribution:calculate-proposals
|
|
{--force : Force recalculate existing proposals}
|
|
{--limit= : Limit number of spatial plannings to process}
|
|
{--skip-existing : Skip spatial plannings that already have proposals}
|
|
{--dry-run : Show what would be processed without actually creating proposals}';
|
|
|
|
/**
|
|
* The console command description.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $description = 'Calculate retribution proposals for all spatial plannings';
|
|
|
|
protected RetributionProposalService $proposalService;
|
|
|
|
public function __construct(RetributionProposalService $proposalService)
|
|
{
|
|
parent::__construct();
|
|
$this->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");
|
|
}
|
|
}
|
|
}
|