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; } }