Files
compras/app/Services/PeopleAccess.php
T
nala c6373365bf
tests / ci (push) Canceled after 0s
commit9
2026-10-30 20:02:30 +02:00

101 lines
6.2 KiB
PHP

<?php
namespace App\Services;
use App\Models\PeopleProcess;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class PeopleAccess
{
public const SCOPES = ['view' => 'Consultar processo', 'prepare' => 'Preparar e gerir candidaturas', 'review' => 'Conferir abertura e avaliar',
'approve' => 'Decidir a selecção', 'contract' => 'Preparar minuta contratual', 'manage' => 'Atribuir equipa deste processo',
'financial' => 'Consultar condições financeiras', 'interviews' => 'Consultar registos de entrevista e evidências', 'contract_view' => 'Consultar minuta contratual'];
/** @return list<string> */
public function scopes(User $actor, PeopleProcess $record): array
{
if ($actor->tenant_id !== $record->tenant_id || ! Schema::hasTable('people_process_access') || ! app(PeopleHiring::class)->allowed($actor, $record->kind)) {
return [];
}
$row = DB::table('people_process_access')->where('tenant_id', $actor->tenant_id)->where('people_process_id', $record->id)->where('user_id', $actor->id)
->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))->first();
return $row === null ? [] : json_decode($row->scopes, true, 16, JSON_THROW_ON_ERROR);
}
public function can(User $actor, PeopleProcess $record, string $scope): bool
{
return in_array($scope, $this->scopes($actor, $record), true);
}
/** @param Builder<PeopleProcess> $query
* @return Builder<PeopleProcess>
*/
public function visible(Builder $query, User $actor): Builder
{
if (! Schema::hasTable('people_process_access')) {
return $query->whereRaw('1 = 0');
}
return $query->whereExists(fn ($q) => $q->selectRaw('1')->from('people_process_access as pa')->whereColumn('pa.people_process_id', 'people_processes.id')
->where('pa.tenant_id', $actor->tenant_id)->where('pa.user_id', $actor->id)->whereJsonContains('pa.scopes', 'view')->where(fn ($q) => $q->whereNull('pa.expires_at')->orWhere('pa.expires_at', '>', now())));
}
public function manages(User $actor, PeopleProcess $record): bool
{
return $actor->tenant_id === $record->tenant_id && ($actor->hasFunctions(['admin']) || $this->can($actor, $record, 'manage'));
}
/** @param list<string> $scopes */
public function insert(PeopleProcess $record, int $user, array $scopes, string $reason): void
{
DB::table('people_process_access')->insert(['tenant_id' => $record->tenant_id, 'people_process_id' => $record->id, 'user_id' => $user,
'scopes' => json_encode($scopes, JSON_THROW_ON_ERROR), 'reason' => $reason, 'created_at' => now(), 'updated_at' => now()]);
}
/** @param array<string,mixed> $input */
public function assign(User $actor, int $id, array $input): void
{
DB::transaction(function () use ($actor, $id, $input) {
app(ConflictGuard::class)->lockCompany($actor);
$actor = app(CurrentActor::class)->lock($actor);
abort_if(app(OrganizationAccess::class)->maintenanceOnly($actor), 403);
$record = PeopleProcess::query()->lockForUpdate()->findOrFail($id);
abort_unless($this->manages($actor, $record), 403);
if (($input['scopes'] ?? null) === []) {
$input['expires_at'] = null;
}
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'user_id' => 'required|integer|min:1', 'scopes' => 'present|array',
'scopes.*' => ['string', 'distinct', Rule::in(array_keys(self::SCOPES))], 'expires_at' => 'nullable|date|after:now', 'reason' => 'required|string|min:10|max:4000'])->validate();
$target = User::query()->where('tenant_id', $actor->tenant_id)->where('id', $data['user_id'])->firstOrFail();
abort_unless($data['scopes'] === [] || app(PeopleHiring::class)->allowed($target, $record->kind), 422, 'Atribua primeiro um perfil compatível com este percurso na parametrização da empresa.');
foreach ($data['scopes'] as $scope) {
$function = match ($scope) {
'review', 'approve' => 'review', 'prepare', 'contract' => 'prepare', default => null
};
if ($function !== null && ! app(PeopleHiring::class)->allowed($target, $record->kind, $function)) {
throw ValidationException::withMessages(['scopes' => 'O perfil desta pessoa não permite a intervenção seleccionada. Ajuste o perfil da empresa ou escolha outra intervenção.']);
}
}
$state = DB::table('people_access_states')->where('people_process_id', $record->id)->lockForUpdate()->first();
if ((int) $data['version'] !== (int) $state->version) {
throw ValidationException::withMessages(['version' => 'A equipa mudou. Actualize antes de guardar.']);
}
$before = DB::table('people_process_access')->where('people_process_id', $record->id)->where('user_id', $target->id)->first();
$scopes = $data['scopes'] === [] ? [] : array_values(array_unique(['view', ...$data['scopes']]));
DB::table('people_process_access')->updateOrInsert(['people_process_id' => $record->id, 'user_id' => $target->id],
['tenant_id' => $record->tenant_id, 'scopes' => json_encode($scopes, JSON_THROW_ON_ERROR), 'expires_at' => isset($data['expires_at']) ? now()->parse($data['expires_at'])->utc()->format('Y-m-d H:i:s') : null,
'reason' => $data['reason'], 'created_at' => $before->created_at ?? now(), 'updated_at' => now()]);
DB::table('people_access_states')->where('id', $state->id)->update(['version' => $state->version + 1]);
app(RequisitionWorkflow::class)->audit($actor, 'people.access', null, $before === null ? null : (array) $before,
['id' => $record->id, 'user_id' => $target->id, 'scopes' => $scopes, 'expires_at' => $data['expires_at'] ?? null, 'reason' => $data['reason']]);
});
}
}