125 lines
6.2 KiB
PHP
125 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\User;
|
|
use App\Support\CheckedFile;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
/** Packaged documentary references prepare private drafts through existing editorial authority. */
|
|
final class FundingProgrammeReferences
|
|
{
|
|
/** @return array{references:array<string,mixed>,sha256:string} */
|
|
private function package(): array
|
|
{
|
|
$references = [];
|
|
$hashes = [];
|
|
foreach (['funding-references.json', 'funding-institutions-nov2026.json'] as $file) {
|
|
$bytes = CheckedFile::read(database_path('data/'.$file));
|
|
$package = json_decode($bytes, true, 512, JSON_THROW_ON_ERROR);
|
|
abort_unless($package['format'] === 'investmoz-funding-references-v1', 503);
|
|
$references = [...$references, ...$package['references']];
|
|
$hashes[$file] = hash('sha256', $bytes);
|
|
}
|
|
|
|
return ['references' => $references, 'sha256' => hash('sha256', json_encode($hashes, JSON_THROW_ON_ERROR))];
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
public function preview(User $actor, string $code): array
|
|
{
|
|
app(FundingProgrammes::class)->guard($actor);
|
|
$package = $this->package();
|
|
$reference = $package['references'][$code] ?? null;
|
|
abort_unless(is_array($reference), 404);
|
|
$unresolved = [];
|
|
$resolve = function (string $name) use (&$unresolved): ?string {
|
|
$matches = app(InvestmentGeography::class)->publicReferences()->where('location_type', 'province')->get(['id', 'name'])->filter(fn ($row) => Str::lower(Str::ascii($row->name)) === Str::lower(Str::ascii($name)));
|
|
if ($matches->count() !== 1) {
|
|
$unresolved[] = $name;
|
|
|
|
return null;
|
|
}
|
|
|
|
return $matches->first()->id;
|
|
};
|
|
if (! empty($reference['territory_names'])) {
|
|
$ids = array_values(array_filter(array_map($resolve, $reference['territory_names'])));
|
|
if (count($ids) === count($reference['territory_names'])) {
|
|
$reference['draft']['territory_ids'] = $ids;
|
|
} else {
|
|
$reference['draft']['coverage'] = 'unknown';
|
|
$reference['draft']['territory_ids'] = [];
|
|
$reference['draft']['coverage_text'] .= ' Correspondência ao cadastro territorial por confirmar.';
|
|
}
|
|
}
|
|
foreach ($reference['location_territory_names'] ?? [] as $index => $name) {
|
|
$reference['draft']['locations'][$index]['territory_id'] = $resolve($name);
|
|
if (! $reference['draft']['locations'][$index]['territory_id']) {
|
|
$reference['draft']['locations'][$index]['evidence_status'] = 'unlocated';
|
|
}
|
|
}
|
|
$plan = ['unresolved_territories' => $unresolved, 'code' => $code, 'edition' => $reference['edition'], 'draft' => $reference['draft'], 'provenance' => $reference['provenance'],
|
|
'package_sha256' => $package['sha256']];
|
|
$plan['hash'] = hash_hmac('sha256', json_encode(Arr::sortRecursive($plan), JSON_THROW_ON_ERROR), config('app.key'));
|
|
|
|
return $plan;
|
|
}
|
|
|
|
/** @return list<array{code:string,title:string}> */
|
|
public function choices(User $actor): array
|
|
{
|
|
app(FundingProgrammes::class)->guard($actor);
|
|
$package = $this->package();
|
|
$choices = [];
|
|
foreach ($package['references'] as $code => $reference) {
|
|
$choices[] = ['code' => $code, 'title' => $reference['draft']['title']];
|
|
}
|
|
|
|
return $choices;
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
public function assisted(User $actor): array
|
|
{
|
|
app(FundingProgrammes::class)->guard($actor);
|
|
$package = $this->package();
|
|
$existing = DB::table('funding_programmes')->get(['id', 'code', 'version', 'published_version_id'])->keyBy('code');
|
|
$items = [];
|
|
foreach ($package['references'] as $code => $reference) {
|
|
$draft = $reference['draft'];
|
|
$row = $existing->get($code);
|
|
$items[] = ['code' => $code, 'title' => $draft['title'], 'summary' => $draft['summary'], 'categories' => $draft['institution_categories'] ?? [],
|
|
'types' => $draft['institution_types'] ?? [], 'status' => $draft['institution_status'] ?? 'reported', 'source_document' => $draft['source_document_name'] ?? null,
|
|
'source_sha256' => $draft['source_document_sha256'] ?? null, 'locations' => $draft['locations'] ?? [], 'contacts' => $draft['contacts'] ?? [],
|
|
'id' => $row?->id, 'published' => $row?->published_version_id !== null];
|
|
}
|
|
|
|
return ['dataset_hash' => $package['sha256'], 'editor_id' => $actor->id, 'items' => $items];
|
|
}
|
|
|
|
/** @return array{id:string,created:bool} */
|
|
public function prepare(User $actor, string $code, string $hash): array
|
|
{
|
|
$plan = $this->preview($actor, $code);
|
|
abort_unless(hash_equals($plan['hash'], $hash), 409, 'As referências mudaram. Recupere a prévia antes de preparar a ficha.');
|
|
|
|
return DB::transaction(function () use ($actor, $code, $plan): array {
|
|
DB::select('select pg_advisory_xact_lock(261102, hashtext(?))', [$code]);
|
|
$existing = DB::table('funding_programmes')->where('code', $code)->lockForUpdate()->first();
|
|
if ($existing) {
|
|
return ['id' => $existing->id, 'created' => false];
|
|
}
|
|
$id = app(FundingProgrammes::class)->save($actor, null, ['version' => 0, 'code' => $code,
|
|
'reason' => 'Preparação de referência documental '.$plan['edition'].'; rascunho sem publicação automática.', 'draft' => $plan['draft']]);
|
|
DB::table('audit_events')->insert(['source_system' => 'funding-catalogue', 'entity_type' => 'funding_programmes', 'entity_id' => $id,
|
|
'actor_user_id' => $actor->id, 'action' => 'reference_prepared', 'data_classification' => 'restricted',
|
|
'after_data' => json_encode(['edition' => $plan['edition'], 'package_sha256' => $plan['package_sha256'], 'provenance' => $plan['provenance'], 'effect' => 'private_draft_only'], JSON_THROW_ON_ERROR)]);
|
|
|
|
return ['id' => $id, 'created' => true];
|
|
});
|
|
}
|
|
}
|