*/ public function portfolioOpportunityIds(InvestmentDossier $dossier): array { if (! $dossier->corridor_id || data_get($dossier->content, 'code') !== 'COR-02') { return []; } // COR-02 is Beira e Centro in investment-portfolio.json. The seven entries are explicitly // grouped under Corredor da Beira in the accepted source, not selected by province/name search. $keys = collect($this->entries())->where('corridor', 'Corredor da Beira')->pluck('key'); return Opportunity::published()->whereIn('management_creation_key', $keys) ->where('workflow_context->corridor_agribusiness->source_sha256', self::SOURCE_HASH)->pluck('id')->all(); } public const LIMITATIONS = 'Promotor, implantação exacta, área, montante, financiamento, viabilidade e licenças não indicados nesta fonte. O corredor é um enquadramento documental; não identifica uma parcela nem confirma disponibilidade.'; /** * @return array */ public function entries(): array { $data = json_decode(CheckedFile::read(database_path('data/corridor-investment-opportunities.json')), true, 512, JSON_THROW_ON_ERROR); if ($data['source_sha256'] !== self::SOURCE_HASH) { throw new \RuntimeException('Fonte divergente; reconciliar antes de cadastrar.'); } $sector = Sector::where('slug', 'agronegocio')->where('is_active', true)->whereNull('deleted_at')->firstOrFail(); $entries = []; foreach ($data['groups'] as $g => $group) { foreach ($group['opportunities'] as $i => $item) { $key = (string) Uuid::uuid5(Uuid::NAMESPACE_URL, 'investmoz:agronegocio:'.self::SOURCE_HASH.':'.$g.':'.$i); $title = Str::limit($item['chain'].' — '.$group['name'], 150, '…'); $description = $item['description']."\n\nEnquadramento documental: ".$group['name'].".\n\nFonte: ".$data['source'].'; SHA-256 '.self::SOURCE_HASH.".\n\n".self::LIMITATIONS; $entries[] = ['key' => $key, 'group' => $g, 'index' => $i, 'corridor' => $group['name'], 'chain' => $item['chain'], 'source' => $data['source'], 'source_sha256' => self::SOURCE_HASH, 'title' => $title, 'summary' => $item['description'], 'description' => $description, 'sector_id' => $sector->id]; } } if (count($entries) !== 34) { throw new \RuntimeException('O lote deve conter as 34 entradas da fonte.'); } return $entries; } /** * @return array */ public function register(User $actor, string $organization): array { // The caller uses the ordinary profile creation route and its approved scope. abort_unless(str_starts_with(request()->path(), 'minhas-oportunidades'), 422); return DB::transaction(function () use ($actor, $organization) { DB::select('SELECT pg_advisory_xact_lock(231026, 34)'); $result = []; foreach ($this->entries() as $entry) { $record = Opportunity::withTrashed()->where('management_creation_key', $entry['key'])->first(); if ($record) { abort_unless(app(PortalProfileAccess::class)->canView($actor, $record), 403); abort_unless(data_get($record->workflow_context, 'corridor_agribusiness.source_sha256') === self::SOURCE_HASH, 409); // Recovery never overwrites editorial changes, withdrawal or archive. $result[] = ['id' => $record->id, 'created' => false, 'key' => $entry['key']]; continue; } $record = app(OpportunityManagement::class)->save($actor, ['creation_key' => $entry['key'], 'organization_id' => $organization, 'opportunity_category' => 'investment_project', 'sector_id' => $entry['sector_id'], 'title' => $entry['title'], 'summary' => $entry['summary'], 'description' => $entry['description'], 'canonical' => ['opportunity_category' => 'investment_project', 'official_title' => $entry['title'], 'executive_summary' => $entry['summary'], 'demand_statement' => $entry['description'], 'sector_id' => $entry['sector_id']], 'reason' => 'Cadastro privado das 34 oportunidades de agronegócio solicitado pela Tempo em 23/10/2026; não constitui publicação nem aprovação de execução.']); $context = $record->workflow_context ?? []; $context['corridor_agribusiness'] = $entry; $context['promotional_summary'] = ['input' => ['title' => $entry['title'], 'summary' => $entry['summary'], 'description' => $entry['description'], 'source_reference' => $entry['source'].'; SHA-256 '.self::SOURCE_HASH, 'limitations' => self::LIMITATIONS, 'edition' => '', 'source_date' => '', 'territory_ids' => [], 'sector_id' => $entry['sector_id']]]; $record->workflow_context = $context; $record->maturity_label = 'Em preparação'; $record->save(); DB::table('audit_events')->insert(['source_system' => 'corridor-agribusiness-registration', 'entity_type' => 'opportunity', 'action' => 'private_source_registered', 'actor_user_id' => $actor->id, 'data_classification' => 'restricted', 'after_data' => json_encode(['record_id' => $record->id, 'source' => $entry, 'no_publication' => true, 'no_master_relation_inferred' => true], JSON_THROW_ON_ERROR)]); $result[] = ['id' => $record->id, 'created' => true, 'key' => $entry['key']]; } return $result; }); } /** * @return array */ public function articleLinks(): array { // Reading an original article must work before the optional sector/opportunity catalogue is loaded. if (! Sector::where('slug', 'agronegocio')->where('is_active', true)->exists()) { return []; } $records = Opportunity::published()->whereIn('management_creation_key', array_column($this->entries(), 'key'))->get()->keyBy('management_creation_key'); $links = []; foreach ($this->entries() as $entry) { if ($record = $records->get($entry['key'])) { $links[$entry['group']][$entry['index']] = '/oportunidades/'.$record->slug; } } return $links; } }