fix: complete InvesteMoz validation and error corrections

This commit is contained in:
2026-10-29 21:47:33 +02:00
parent 63147312e2
commit daf8623441
718 changed files with 127308 additions and 20959 deletions
+26 -8
View File
@@ -4,6 +4,7 @@ namespace App\Services;
use App\Models\Opportunity;
use App\Models\User;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
@@ -11,12 +12,15 @@ use Illuminate\Support\Facades\Validator;
/** An existing opportunity resource references the original document. No binary or permission is copied. */
class OpportunityLibrarySupport
{
/**
* @return array<array-key, mixed>
*/
public function choices(User $actor): array
{
return $this->sources($actor)->orderBy('r.id')->limit(100)->get(['r.id', 'r.title', 'r.version_label', 'r.metadata_version', 'r.published', 'd.sha256'])->all();
}
private function sources(User $actor)
private function sources(User $actor): Builder
{
return DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
->whereNull('d.deleted_at')->where('d.status', '!=', 'deleted')
@@ -24,18 +28,21 @@ class OpportunityLibrarySupport
->whereIn('d.data_classification', ['internal', 'public'])
->when(! $actor->can('manage-portal'), function ($query) use ($actor) {
$query->where(function ($scope) use ($actor) {
$scope->where(fn ($public) => $public->where('r.published',true)->where('d.data_classification','public')->where('d.status','active')->where('d.malware_scan_status','clean')->whereColumn('d.sha256','d.malware_scanned_sha256'));
$scope->where(fn ($public) => $public->where('r.published', true)->where('d.data_classification', 'public')->where('d.status', 'active')->where('d.malware_scan_status', 'clean')->whereColumn('d.sha256', 'd.malware_scanned_sha256'));
if ($actor->hasVerifiedEmail()) {
$scope->orWhere(fn ($own) => $own->where('r.created_by',$actor->id)->where('d.uploaded_by',$actor->id));
$scope->orWhere(fn ($own) => $own->where('r.created_by', $actor->id)->where('d.uploaded_by', $actor->id));
}
$editorial=app(DocumentManagementAccess::class);
$editorial = app(DocumentManagementAccess::class);
if ($editorial->editorial($actor)) {
$scope->orWhereIn('r.id',$editorial->editorialResources($actor)->select('r.id'));
$scope->orWhereIn('r.id', $editorial->editorialResources($actor)->select('r.id'));
}
});
});
}
/**
* @param array<array-key, mixed> $input
*/
public function attach(User $actor, Opportunity $opportunity, array $input): \stdClass
{
app(PortalProfileAccess::class)->authorize($actor, 'edit', $opportunity);
@@ -45,12 +52,13 @@ class OpportunityLibrarySupport
$opportunity = Opportunity::whereKey($opportunity->id)->lockForUpdate()->firstOrFail();
app(PortalProfileAccess::class)->authorize($actor, 'edit', $opportunity);
$source = $this->sources($actor)->where('r.id', $data['resource_id'])->lockForUpdate()->first(['r.*', 'd.id as original_id', 'd.sha256', 'd.version_no']);
abort_unless($source, 403, 'O acesso actual à biblioteca não permite reutilizar este original.');
abort_unless($source !== null, 403, 'O acesso actual à biblioteca não permite reutilizar este original.');
$existing = DB::table('opportunity_documents')->where('opportunity_id', $opportunity->id)->whereNull('deleted_at')->where('document_id', $source->original_id)->first();
if ($existing) {
$payload = json_decode($existing->payload, true);
abort_unless(($payload['library_source']['resource_id'] ?? null) === $source->id, 409, 'Original já associado por outro percurso; recuperar o suporte existente.');
abort_unless(($payload['section'] ?? null) === ($data['section'] ?? null) && ($payload['supported_statement'] ?? null) === ($data['supported_statement'] ?? null), 409, 'Original já associado com outra referência; editar explicitamente o suporte existente.');
return $existing;
}
abort_unless(hash_equals(app(OpportunityManagement::class)->etag($opportunity), $data['etag']), 409, 'A ficha mudou; preserve a selecção e recupere a versão actual.');
@@ -71,6 +79,7 @@ class OpportunityLibrarySupport
'reason' => Crypt::encryptString($data['reason']), 'snapshot' => Crypt::encryptString(json_encode($after, JSON_THROW_ON_ERROR))])]);
$opportunity->management_version++;
$opportunity->save();
return $after;
});
}
@@ -79,19 +88,28 @@ class OpportunityLibrarySupport
{
$payload = is_string($resource->payload) ? json_decode($resource->payload, true) : $resource->payload;
$pin = $payload['library_source'] ?? null;
if (! $pin) return;
if (! $pin) {
return;
}
$source = DB::table('portal_resources')->where('id', $pin['resource_id'])->first();
abort_unless($source && $source->rights_status !== 'restricted' && $document->rights_status !== 'restricted'
&& in_array($document->data_classification, ['internal', 'public'], true)
&& $document->id === $pin['document_id'] && hash_equals($pin['sha256'], trim($document->sha256)), 403, 'O original está retirado, restrito ou já não corresponde à versão guardada.');
}
/**
* @return array<array-key, mixed>
*/
public function snapshot(User $actor, Opportunity $opportunity): array
{
$access = app(PortalProfileAccess::class);
if (! $access->canView($actor, $opportunity)) return [];
if (! $access->canView($actor, $opportunity)) {
return [];
}
return collect(app(OpportunityResources::class)->listing($opportunity->id, 'documents'))->filter(fn ($r) => ! $r->deleted_at && isset($r->payload['library_source']))->map(function ($r) use ($opportunity) {
$pin = $r->payload['library_source'];
return [...$pin, 'title' => $r->payload['title'], 'support_id' => $r->id, 'support_version' => $r->version,
'section' => $r->payload['section'] ?? null, 'opportunity_id' => $opportunity->id,
'download_url' => '/minhas-oportunidades/'.$opportunity->id.'/resources/documents/'.$r->id.'/download?source_sha256='.$pin['sha256']];