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

119 lines
7.9 KiB
PHP

<?php
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;
/** 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): 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')
->where('r.rights_status', '!=', 'restricted')->where('d.rights_status', '!=', 'restricted')
->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'));
if ($actor->hasVerifiedEmail()) {
$scope->orWhere(fn ($own) => $own->where('r.created_by', $actor->id)->where('d.uploaded_by', $actor->id));
}
$editorial = app(DocumentManagementAccess::class);
if ($editorial->editorial($actor)) {
$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);
$data = Validator::make($input, ['resource_id' => 'required|integer|min:1', 'creation_key' => 'required|uuid', 'etag' => 'required|string|size:64', 'reason' => 'required|string|min:20|max:1000', 'section' => 'nullable|string|max:255', 'supported_statement' => 'nullable|string|max:2000'])->validate();
return DB::transaction(function () use ($actor, $opportunity, $data) {
$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 !== 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.');
abort_if(DB::table('opportunity_documents')->where('id', $data['creation_key'])->exists(), 409, 'Chave já utilizada noutro suporte.');
abort_if(DB::table('opportunity_documents')->where('opportunity_id', $opportunity->id)->whereNull('deleted_at')->count() >= 100, 422, 'Limite de documentos atingido.');
$payload = ['title' => $source->title, 'category' => $source->category, 'issuer' => null, 'edition' => $source->version_label,
'classification' => 'internal', 'visibility' => 'internal', 'source_reference' => $source->source_reference,
'section' => $data['section'] ?? null, 'supported_statement' => $data['supported_statement'] ?? null,
'library_source' => ['resource_id' => $source->id, 'document_id' => $source->original_id, 'metadata_version' => $source->metadata_version,
'document_version' => $source->version_no, 'edition' => $source->version_label, 'sha256' => trim($source->sha256), 'source_reference' => $source->source_reference]];
$now = now();
DB::table('opportunity_documents')->insert(['id' => $data['creation_key'], 'opportunity_id' => $opportunity->id, 'document_id' => $source->original_id,
'payload' => json_encode($payload, JSON_THROW_ON_ERROR), 'version' => 1, 'created_by' => $actor->id, 'created_at' => $now, 'updated_at' => $now]);
$after = DB::table('opportunity_documents')->where('id', $data['creation_key'])->first();
DB::table('audit_events')->insert(['source_system' => 'opportunity-resources', 'entity_type' => 'documents', 'actor_user_id' => $actor->id,
'action' => 'library_original_reused', 'data_classification' => 'restricted', 'before_data' => json_encode(['snapshot' => Crypt::encryptString('null')]),
'after_data' => json_encode(['opportunity_id' => $opportunity->id, 'resource_id' => $after->id, 'version' => 1,
'reason' => Crypt::encryptString($data['reason']), 'snapshot' => Crypt::encryptString(json_encode($after, JSON_THROW_ON_ERROR))])]);
$opportunity->management_version++;
$opportunity->save();
return $after;
});
}
public function assertAvailable(\stdClass $resource, \stdClass $document): void
{
$payload = is_string($resource->payload) ? json_decode($resource->payload, true) : $resource->payload;
$pin = $payload['library_source'] ?? null;
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 [];
}
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']];
})->values()->all();
}
}