Files
Investidor-V2/app/Services/CorridorAgribusinessOpportunities.php
T

131 lines
7.1 KiB
PHP

<?php
namespace App\Services;
use App\Models\InvestmentDossier;
use App\Models\Opportunity;
use App\Models\Sector;
use App\Models\User;
use App\Support\CheckedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Ramsey\Uuid\Uuid;
/** Source-defined opportunities; never infer a master corridor, site or promoter. */
final class CorridorAgribusinessOpportunities
{
public const SOURCE_HASH = '1473b06c48aa234e771660e3f2a51b9d2e4e1943bd52d878e0ae854ee6657055';
/** Documentary cross-reference only; keep the two source designations and master records intact.
* @return array<array-key, mixed>
*/
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<array-key, mixed>
*/
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<array-key, mixed>
*/
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<array-key, mixed>
*/
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;
}
}