110 lines
6.1 KiB
PHP
110 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Services\InvestmentZones;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Str;
|
|
use RuntimeException;
|
|
|
|
class ImportIndustrialParkCandidates extends Command
|
|
{
|
|
protected $signature = 'investment-zones:import-industrial-candidates {--apply : Create missing private drafts} {--actor= : Existing eligible manager user ID required with --apply}';
|
|
|
|
protected $description = 'Reconcile and optionally create source-backed industrial-park/ZFI drafts without publishing';
|
|
|
|
public function handle(): int
|
|
{
|
|
$document = json_decode(file_get_contents(database_path('data/industrial-park-candidate-drafts.json')), true, flags: JSON_THROW_ON_ERROR);
|
|
if (($document['source_document_attached'] ?? null) !== true
|
|
|| ($document['source_document_reconciled'] ?? null) !== true) {
|
|
$this->error('The source document must be attached and reconciled before importing drafts. No records written.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
$rows = [];
|
|
foreach ($document['records'] as $record) {
|
|
$existing = DB::table('investment_zones')->where('code', $record['code'])->first(['id', 'name', 'publication_status']);
|
|
$territories = [];
|
|
foreach ($record['territories'] as $territory) {
|
|
$expected = $this->normalize($territory['name']);
|
|
$matches = DB::table('geo_locations')->whereNull('deleted_at')->where('location_type', $territory['type'])
|
|
->get(['id', 'name'])->filter(fn ($node) => $this->normalize($node->name) === $expected);
|
|
if ($matches->count() !== 1) {
|
|
throw new RuntimeException("Territory {$territory['name']} ({$territory['type']}) matched {$matches->count()} rows; approximate matching is disabled.");
|
|
}
|
|
$territories[] = [...$territory, 'geo_location_id' => $matches->first()->id, 'resolved_name' => $matches->first()->name];
|
|
}
|
|
$rows[] = ['record' => $record, 'existing' => $existing, 'territories' => $territories];
|
|
}
|
|
$this->table(['Code', 'Action', 'Territory IDs resolved', 'Areas'], array_map(fn ($row) => [
|
|
$row['record']['code'], $row['existing'] ? 'reconcile/skip-existing' : ($this->option('apply') ? 'create-private-draft' : 'would-create-private-draft'),
|
|
implode('; ', array_map(fn ($t) => $t['resolved_name'], $row['territories'])), count($row['record']['areas']),
|
|
], $rows));
|
|
|
|
if (! $this->option('apply')) {
|
|
$this->info('Dry run only. No writes made; every created record remains a private draft, with legal regime/availability not implied by the source.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
if (! Schema::hasColumn('investment_zones', 'source_evidence')) {
|
|
$this->error('The additive industrial-park schema is not migrated in this database. No records written.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$actor = filter_var($this->option('actor'), FILTER_VALIDATE_INT);
|
|
$user = $actor ? DB::table('users')->where('id', $actor)->first(['id', 'email', 'email_verified_at', 'two_factor_confirmed_at']) : null;
|
|
if (! $user || ! $user->email_verified_at || ! $user->two_factor_confirmed_at
|
|
|| ! in_array(mb_strtolower($user->email), config('portal.managers', []), true)) {
|
|
$this->error('An existing verified, MFA-enabled configured manager is required. No records written.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$created = DB::transaction(function () use ($rows, $actor, $document): array {
|
|
$created = [];
|
|
foreach ($rows as $row) {
|
|
if ($row['existing']) {
|
|
continue;
|
|
}
|
|
$record = $row['record'];
|
|
$source = $record['source'];
|
|
$sourceReference = $source['url'].' — '.$source['title'];
|
|
$locations = array_map(fn ($location) => ['geo_location_id' => $location['geo_location_id'], 'scope_type' => $location['scope_type'],
|
|
'source_reference' => $sourceReference, 'legal_source_id' => null], $row['territories']);
|
|
$first = $row['territories'][0]['geo_location_id'] ?? null;
|
|
$zone = app(InvestmentZones::class)->save(null, [
|
|
'code' => $record['code'], 'name' => $record['name'], 'category' => $record['category'],
|
|
'physical_category' => $record['physical_category'], 'legal_regime' => $record['legal_regime'],
|
|
'development_status' => $record['development_status'], 'availability_status' => $record['availability_status'],
|
|
'operational_status' => 'unknown', 'description' => $record['source']['supports'].(! empty($record['territory_note']) ? ' '.$record['territory_note'] : ''),
|
|
'source_evidence' => [[...$source, 'source_date' => null]],
|
|
'area_components' => array_map(fn ($area) => [...$area, 'source_reference' => $sourceReference.' — '.$area['source_reference'], 'source_date' => null], $record['areas']),
|
|
'area_value' => null, 'area_unit' => null, 'activities' => [], 'resources' => [], 'aliases' => [],
|
|
'sources_consulted_at' => $document['consulted_at'],
|
|
'legal_source_id' => null, 'parent_zone_id' => null, 'primary_territory_id' => $first,
|
|
'territory_version' => $first ? 1 : 0, 'locations' => $locations,
|
|
], (int) $actor);
|
|
$created[] = [$zone->code, $zone->id];
|
|
}
|
|
|
|
return $created;
|
|
});
|
|
$this->info('Private drafts created: '.count($created).'. Existing codes skipped. No legal sources were approved and no records were published.');
|
|
foreach ($created as [$code, $id]) {
|
|
$this->line($code.' '.$id);
|
|
}
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function normalize(string $value): string
|
|
{
|
|
return Str::upper(Str::ascii(trim($value)));
|
|
}
|
|
}
|