readSource(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))); } private function readSource(string $path): string { $bytes = file_get_contents($path); if ($bytes === false) { throw new RuntimeException('Source file is unavailable: '.$path); } return $bytes; } }