134 lines
8.3 KiB
PHP
134 lines
8.3 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Services\InvestmentZones;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use RuntimeException;
|
|
|
|
class ImportInvestmentZones extends Command
|
|
{
|
|
protected $signature = 'investment-zones:import-initial {--apply : Create missing records as private drafts} {--actor= : Existing manager user ID required with --apply}';
|
|
|
|
protected $description = 'Import documented initial ZEE, ZFI and industrial park references without overwriting or publishing existing records';
|
|
|
|
public function handle(): int
|
|
{
|
|
$path = database_path('data/investment-zones-initial.json');
|
|
$document = json_decode($this->readSource($path), true, flags: JSON_THROW_ON_ERROR);
|
|
$rows = [];
|
|
foreach ($document['records'] as $record) {
|
|
$existing = DB::table('investment_zones')->where('code', $record['code'])->first(['id', 'name', 'category', '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', 'data_classification'])->filter(fn ($node) => $this->normalize($node->name) === $expected);
|
|
if ($matches->count() !== 1) {
|
|
throw new RuntimeException("Territory reference {$territory['name']} ({$territory['type']}) matched {$matches->count()} master records; no approximate match or new territory was created.");
|
|
}
|
|
$territories[] = [...$territory, 'geo_location_id' => $matches->first()->id, 'resolved_name' => $matches->first()->name];
|
|
}
|
|
$rows[] = ['code' => $record['code'], 'name' => $record['name'], 'action' => $existing ? 'skip-existing' : ($this->option('apply') ? 'create-draft' : 'would-create-draft'), 'existing' => $existing, 'territories' => $territories];
|
|
}
|
|
|
|
if (! $this->option('apply')) {
|
|
$this->table(['Code', 'Name', 'Action', 'Territories'], array_map(fn ($row) => [$row['code'], $row['name'], $row['action'], implode('; ', array_column($row['territories'], 'resolved_name'))], $rows));
|
|
$this->info('Dry run: no records written. Sources are official references; records remain private drafts pending review.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$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 portal manager is required with --apply --actor=.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$summary = DB::transaction(function () use ($rows, $document, $actor): array {
|
|
$created = [];
|
|
$skipped = [];
|
|
foreach ($rows as $row) {
|
|
if ($row['existing']) {
|
|
$skipped[] = $row['code'];
|
|
|
|
continue;
|
|
}
|
|
$record = collect(is_array($document['records'] ?? null) ? $document['records'] : [])->firstWhere('code', $row['code']);
|
|
$requiresLegalSource = in_array($record['category'], ['zee', 'zfi'], true);
|
|
$source = $requiresLegalSource
|
|
? DB::table('investment_legal_sources')->where('url', $record['source']['url'])->where('diploma', $record['source']['diploma'])->first()
|
|
: null;
|
|
$sourceId = $requiresLegalSource ? ($source->id ?? (string) Str::uuid()) : null;
|
|
if ($requiresLegalSource && ! $source) {
|
|
DB::table('investment_legal_sources')->insert([
|
|
'id' => $sourceId, ...$record['source'], 'validation_status' => 'pending', 'owner_id' => $actor,
|
|
'version_no' => 1, 'created_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
}
|
|
$parentId = null;
|
|
if (isset($record['parent_code'])) {
|
|
$parentId = DB::table('investment_zones')->where('code', $record['parent_code'])->value('id');
|
|
if (! $parentId) {
|
|
throw new RuntimeException("Parent zone {$record['parent_code']} must be imported before {$record['code']}.");
|
|
}
|
|
}
|
|
$locations = array_map(fn ($location) => [
|
|
'geo_location_id' => $location['geo_location_id'], 'scope_type' => $location['scope_type'],
|
|
'source_reference' => $record['source']['url'].' — '.($record['source']['diploma'] ?? $record['source']['title']), 'legal_source_id' => $sourceId,
|
|
], $row['territories']);
|
|
$firstLocation = $row['territories'][0]['geo_location_id'] ?? null;
|
|
$zone = app(InvestmentZones::class)->save(null, [
|
|
'code' => $record['code'], 'category' => $record['category'], 'physical_category' => $record['physical_category'],
|
|
'legal_regime' => $record['legal_regime'], 'operational_status' => $record['operational_status'],
|
|
'development_status' => $record['development_status'] ?? match ($record['operational_status']) {
|
|
'operational' => 'operational', 'implementation' => 'in_development', 'planned' => 'planned', default => 'unknown'
|
|
},
|
|
'availability_status' => $record['availability_status'] ?? 'not_informed',
|
|
'name' => $record['name'], 'name_en' => $record['name_en'], 'description' => $record['description'], 'description_en' => $record['description_en'],
|
|
'area_value' => $record['area_value'], 'area_unit' => $record['area_unit'], 'area_components' => $record['area_components'] ?? [],
|
|
'source_evidence' => [[
|
|
'title' => $record['source']['title'], 'url' => $record['source']['url'],
|
|
'source_type' => str_contains($record['source']['url'], 'inm.gov.mz') ? 'government' : (str_contains($record['source']['url'], 'apiex.gov.mz') ? 'institutional' : 'promoter'),
|
|
'supports' => $record['source']['supports'] ?? 'Referência institucional/promocional; não comprova regime jurídico, incentivos ou disponibilidade.',
|
|
'source_date' => $record['source']['source_date'] ?? null,
|
|
]], 'aliases' => $record['aliases'],
|
|
'activities' => $record['activities'], 'resources' => [], 'historical_territory' => $record['historical_territory'] ?? null,
|
|
'data_as_of' => null, 'sources_consulted_at' => $document['consulted_at'], 'operator_id' => null,
|
|
'legal_source_id' => $sourceId, 'parent_zone_id' => $parentId, 'primary_territory_id' => $firstLocation,
|
|
'territory_version' => $firstLocation ? 1 : 0, 'province' => null, 'district' => null, 'locations' => $locations,
|
|
], (int) $actor);
|
|
$created[] = ['code' => $record['code'], 'id' => $zone->id, 'source' => $source?->id ? 'reused' : 'created', 'locations_pending' => count($locations)];
|
|
}
|
|
|
|
return compact('created', 'skipped');
|
|
});
|
|
|
|
$this->table(['Code', 'ID', 'Source', 'Pending locations'], array_map(fn ($row) => [$row['code'], $row['id'], $row['source'], $row['locations_pending']], $summary['created']));
|
|
$this->info('Created drafts: '.count($summary['created']).'; skipped without overwrite: '.count($summary['skipped']).'; publication: none.');
|
|
$this->line('Skipped: '.implode(', ', $summary['skipped']));
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function normalize(string $value): string
|
|
{
|
|
return Str::upper(Str::ascii(trim($value)));
|
|
}
|
|
|
|
private function readSource(string $path): string
|
|
{
|
|
$bytes = file_get_contents($path);
|
|
if ($bytes === false) {
|
|
throw new RuntimeException('Source file is unavailable: '.$path);
|
|
}
|
|
|
|
return $bytes;
|
|
}
|
|
}
|