131 lines
7.1 KiB
PHP
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;
|
|
}
|
|
}
|