vrsao 2
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-08 14:02:01 +02:00
parent bdd766c68a
commit 1106509116
354 changed files with 48275 additions and 24635 deletions
@@ -8,10 +8,11 @@ use App\Models\Sector;
use App\Services\IdempotencyConflict;
use App\Services\InterestSubmission;
use App\Services\Malware\MalwareScanner;
use App\Services\VirtualDesk;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\QueryException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
@@ -47,7 +48,7 @@ class PortalServicesController extends Controller
->where('d.malware_scan_status', 'clean')->whereColumn('d.malware_scanned_sha256', 'd.sha256');
}
public function page(Request $request): Response|JsonResponse
public function page(Request $request): Response|JsonResponse|RedirectResponse
{
$page = $request->route()->defaults['page'];
if ($page === 'partnerships' && Schema::hasColumn('portal_partnerships', 'partnership_type')) {
@@ -105,12 +106,12 @@ class PortalServicesController extends Controller
}
$pageResult = $query->orderByRaw("CASE WHEN r.document_type IN ('official_form','digital_adaptation','guidance_template') THEN 1 ELSE 0 END")
->orderByDesc('r.essential')->orderBy('r.title')->select([
'r.id', 'r.document_id', 'r.title', 'r.description', 'r.category', 'r.topic', 'r.document_type', 'r.document_number', 'r.document_date',
'r.applicable_from', 'r.applicable_until',
'r.source_url', 'r.language', 'r.version_label', 'r.document_group_id', 'r.territorial_scope', 'r.territory_id',
'r.source_confirmation', 'r.legal_status', 'r.rights_status', 'r.last_reviewed_at', 'r.last_reviewed_by', 'r.independent_review', 'r.metadata_version', 'r.source_reference', 'r.essential',
'd.file_name', 'd.media_type', 'd.size_bytes', 'd.issuer', 'd.issued_on', 'd.valid_until', 'd.version_no',
])->paginate(20)->withQueryString();
'r.id', 'r.document_id', 'r.title', 'r.description', 'r.category', 'r.topic', 'r.document_type', 'r.document_number', 'r.document_date',
'r.applicable_from', 'r.applicable_until',
'r.source_url', 'r.language', 'r.version_label', 'r.document_group_id', 'r.territorial_scope', 'r.territory_id',
'r.source_confirmation', 'r.legal_status', 'r.rights_status', 'r.last_reviewed_at', 'r.last_reviewed_by', 'r.independent_review', 'r.metadata_version', 'r.source_reference', 'r.essential',
'd.file_name', 'd.media_type', 'd.size_bytes', 'd.issuer', 'd.issued_on', 'd.valid_until', 'd.version_no',
])->paginate(20)->withQueryString();
$pageResult->getCollection()->transform(function ($resource) {
$resource->links = DB::table('portal_resource_links')->where('resource_id', $resource->id)
->orderBy('target_type')->orderBy('target_key')->get($this->resourceLinkColumns());
@@ -153,6 +154,9 @@ class PortalServicesController extends Controller
$props['partnerships'] = $query->orderByDesc('id')->select(['id', 'title', 'sector', 'province', 'offer', 'seeking'])->paginate(20)->withQueryString();
}
if ($page === 'desk') {
if (app(VirtualDesk::class)->installed()) {
return redirect('/balcao-virtual/painel');
}
$props['requests'] = InterestRequest::where('metadata->portal_user_id', $request->user()->id)->latest()->limit(100)->get(['reference', 'status', 'created_at', 'metadata']);
$props['myPartnerships'] = DB::table('portal_partnerships')->where('user_id', $request->user()->id)->latest()->limit(100)->get(['id', 'title', 'status']);
}
@@ -161,7 +165,7 @@ class PortalServicesController extends Controller
return Inertia::render('portal/services', $props);
}
public function recommendations(Request $request)
public function recommendations(Request $request): JsonResponse
{
$data = $request->validate([
'activity_id' => ['nullable', 'string', 'max:180'], 'province_id' => ['nullable', 'uuid'],
@@ -199,10 +203,11 @@ class PortalServicesController extends Controller
? DB::table('portal_resource_links')->where('resource_id', $resource->id)->where('target_type', 'requirement')
->where('review_status', 'confirmed')->orderBy('target_key')->get(['target_key', 'basis_reference', 'review_status'])
: collect();
return [...(array) $resource,
'download_url' => route('services.download', ['id' => $resource->id]),
'recommendation_reason' => $matched?->condition_text ?? ('Relacionado com '.$matched?->target_type.' '.$matched?->target_key.'; confirme a aplicabilidade ao seu projecto.'),
'applicability' => $matched?->applicability ?? 'support',
'recommendation_reason' => $matched->condition_text ?? ('Relacionado com '.$matched?->target_type.' '.$matched?->target_key.'; confirme a aplicabilidade ao seu projecto.'),
'applicability' => $matched->applicability ?? 'support',
'basis_reference' => $matched?->basis_reference,
'documentary_evidence' => $evidence->map(fn ($link) => ['key' => $link->target_key,
'label' => config('document-requirements.catalogue.'.$link->target_key.'.label', $link->target_key),
@@ -357,7 +362,7 @@ class PortalServicesController extends Controller
return Inertia::render('portal/manage', ['resources' => $resources,
'requirementCatalogue' => $this->requirementEvidenceEnabled()
? collect(config('document-requirements.catalogue'))->map(fn ($item, $key) => ['key' => $key, ...$item])->values()
? collect(is_array($catalogue = config('document-requirements.catalogue', [])) ? $catalogue : [])->map(fn ($item, $key) => ['key' => $key, ...$item])->values()
: collect(),
'requirementAssociationsEnabled' => $this->requirementEvidenceEnabled(),
'partnerships' => DB::table('portal_partnerships')->latest()->limit(100)->get(),
@@ -548,10 +553,12 @@ class PortalServicesController extends Controller
]);
if ($resource === null) {
$skipped[$resourceId] = 'Registo ou ficheiro inexistente.';
continue;
}
if ($resource->published && $resource->data_classification === 'public' && $resource->resource_rights === 'authorized' && $resource->file_rights === 'authorized') {
$published[$resourceId] = 'Já publicado.';
continue;
}
$path = Storage::disk('local')->path($resource->storage_key);
@@ -565,12 +572,14 @@ class PortalServicesController extends Controller
|| ! hash_equals((string) $resource->sha256, $actualHash)
|| ! hash_equals((string) $resource->sha256, (string) $resource->malware_scanned_sha256)) {
$skipped[$resourceId] = 'Ficheiro indisponível, não limpo ou diferente dos bytes analisados.';
continue;
}
if (in_array($resource->resource_rights, ['restricted'], true) || in_array($resource->file_rights, ['restricted'], true)
|| $resource->source_confirmation === 'unconfirmed' || ! filter_var($resource->source_url, FILTER_VALIDATE_URL)
|| parse_url($resource->source_url, PHP_URL_SCHEME) !== 'https') {
$skipped[$resourceId] = 'Restrição de divulgação ou origem identificada em falta.';
continue;
}
@@ -600,6 +609,7 @@ class PortalServicesController extends Controller
->with('disclosureResults', ['published' => $published, 'skipped' => $skipped]);
}
/** @param array<string,mixed> $additional */
private function audit(Request $request, string $action, string $table, int $id, array $additional = []): void
{
DB::table('audit_events')->insert(['source_system' => 'portal-services', 'entity_type' => $table,
@@ -641,6 +651,7 @@ class PortalServicesController extends Controller
}
}
/** @return array<string,mixed> */
private function validateResource(Request $request): array
{
$data = $this->validateManagement($request, [
@@ -691,6 +702,9 @@ class PortalServicesController extends Controller
/** Keep validation redirects on the management GET page even if the request
* arrived with a stale Referer pointing at a write-only resource endpoint.
*
* @param array<string,mixed> $rules
* @return array<string,mixed>
*/
private function validateManagement(Request $request, array $rules): array
{
@@ -701,6 +715,7 @@ class PortalServicesController extends Controller
}
}
/** @param array<string,string|list<string>> $errors */
private function managementValidationException(array $errors): ValidationException
{
$exception = ValidationException::withMessages($errors);
@@ -709,6 +724,8 @@ class PortalServicesController extends Controller
return $exception;
}
/** @param array<string,mixed> $data
* @return array<string,mixed> */
private function resourceFields(array $data, int $actor, ?string $identity, ?int $duplicate, bool $creating): array
{
unset($data['associations'], $data['issuer'], $data['data_classification']);
@@ -725,6 +742,7 @@ class PortalServicesController extends Controller
return $data;
}
/** @param list<array<string,mixed>> $links */
private function syncResourceLinks(int $resourceId, array $links): void
{
foreach ($links as $link) {
@@ -741,7 +759,8 @@ class PortalServicesController extends Controller
}
}
private function assertPublicClassificationEligible(array $data, object $document): void
/** @param array<string,mixed> $data */
private function assertPublicClassificationEligible(array $data, \stdClass $document): void
{
if (($data['data_classification'] ?? $document->data_classification) !== 'public') {
return;
@@ -756,6 +775,7 @@ class PortalServicesController extends Controller
'Identifique e confirme a fonte antes de classificar o ficheiro como público.');
}
/** @param array<string,mixed> $data */
private function identityKey(array $data): ?string
{
if (empty($data['issuer']) || empty($data['document_number'])) {
@@ -782,6 +802,7 @@ class PortalServicesController extends Controller
&& DB::table('migrations')->where('migration', '2026_10_06_200000_add_document_requirement_evidence_links')->exists();
}
/** @return list<string> */
private function resourceLinkColumns(): array
{
$columns = ['target_type', 'target_key', 'relationship', 'applicability', 'condition_text', 'basis_reference'];
@@ -792,9 +813,10 @@ class PortalServicesController extends Controller
return $columns;
}
/** @return list<string> */
private function territoryScopeIds(string $id): array
{
return collect(DB::select(<<<'SQL'
$rows = DB::select(<<<'SQL'
WITH RECURSIVE territory_tree AS (
SELECT id FROM geo_locations WHERE id = ? AND deleted_at IS NULL AND data_classification = 'public'
UNION ALL
@@ -803,6 +825,15 @@ WITH RECURSIVE territory_tree AS (
WHERE child.deleted_at IS NULL AND child.data_classification = 'public'
)
SELECT id FROM territory_tree
SQL, [$id]))->pluck('id')->all() ?: [$id];
SQL, [$id]);
$ids = [];
foreach ($rows as $row) {
if (! is_string($row->id)) {
throw new \RuntimeException('Territory identifier must be a string.');
}
$ids[] = $row->id;
}
return $ids ?: [$id];
}
}