Files
compras/app/Services/OrganizationAccess.php
T
2026-11-02 08:08:05 +02:00

154 lines
8.4 KiB
PHP

<?php
namespace App\Services;
use App\Enums\PurchasingRole;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class OrganizationAccess
{
public const PERMISSIONS = [
'warehouse_request_review' => 'Autorizar ou rejeitar requisições internas ao armazém do departamento, de forma independente',
'warehouse_request_deliver' => 'Entregar material de requisições internas autorizadas (exige função de armazém)',
'warehouse_request_prepare' => 'Preparar requisições próprias ao armazém com destino de utilização',
'warehouse_request_read' => 'Consultar requisições ao armazém da empresa',
'warehouse_availability_read' => 'Consultar disponibilidade e necessidades planeadas para requisições ao armazém',
'inventory_lot_read' => 'Consultar lotes, validade e quarentena',
'inventory_lot_prepare' => 'Preparar controlo por lotes e correcções de identificação',
'inventory_lot_review' => 'Conferir activação e parâmetros do controlo por lotes',
'inventory_lot_hold' => 'Colocar lotes em quarentena',
'inventory_lot_release' => 'Conferir identificação e libertar lotes de forma independente',
'inventory_adjust_review' => 'Conferir e aplicar ajustes físicos por lote de forma independente',
'inventory_disposition_read' => 'Consultar devoluções a fornecedores e abates físicos',
'inventory_disposition_prepare' => 'Preparar devoluções a fornecedores e abates físicos',
'inventory_disposition_review' => 'Conferir devoluções e abates de forma independente',
'inventory_reversal_read' => 'Consultar estornos integrais de stock e respectivas origens',
'inventory_reversal_prepare' => 'Preparar estornos de stock ligados ao original',
'inventory_reversal_review' => 'Conferir estornos de stock de forma independente',
'supply_read' => 'Consultar fornecimentos e calendário de reposição',
'supply_prepare' => 'Preparar acordos habituais e políticas de reposição',
'supply_review' => 'Conferir e aprovar acordos e políticas de reposição',
'supply_suggest_prepare' => 'Preparar e actualizar sugestões de reposição',
'supply_suggest_review' => 'Conferir e decidir sugestões de reposição',
'supply_suggest_convert' => 'Converter sugestões aprovadas em requisições próprias',
'supply_history_prepare' => 'Preparar análise e cobertura do histórico de consumo',
'supply_history_review' => 'Conferir o histórico de consumo de forma independente',
'supply_reserve' => 'Reservar necessidades e conferir satisfação por saídas',
'supply_delivery' => 'Registar confirmações e replaneamento de entregas',
'supply_technical_prepare' => 'Preparar equivalências técnicas de artigos',
'supply_technical_review' => 'Validar equivalências técnicas de forma independente',
'import_read' => 'Consultar processos de importação',
'import_prepare' => 'Preparar e acompanhar logística de importações',
'import_customs' => 'Conferir documentação e libertação aduaneira',
'requester' => 'Solicitar e acompanhar compras próprias',
'department_head' => 'Supervisão e aprovação do departamento',
'procurement' => 'Preparação e gestão operacional de compras',
'finance' => 'Conferência e operações financeiras',
'warehouse' => 'Recepção e operações de armazém',
'admin' => 'Administrar a empresa, utilizadores e parametrização',
'people_service_prepare' => 'Preparar contratação de prestadores individuais',
'people_service_review' => 'Avaliar e conferir contratação de prestadores individuais',
'people_staff_prepare' => 'Preparar recrutamento de trabalhadores',
'people_staff_review' => 'Avaliar e conferir recrutamento de trabalhadores',
'people_registry_read' => 'Consultar cadastro partilhado de pessoas da empresa',
'people_registry_manage' => 'Criar e actualizar cadastro partilhado de pessoas da empresa',
];
public function active(?int $tenant): bool
{
return $tenant !== null && Schema::hasTable('organization_access') && DB::table('organization_access')->where('tenant_id', $tenant)->where('active', true)->exists();
}
/** @return list<string> */
public function permissions(User $user, ?string $context = null): array
{
if ($user->tenant_id === null) {
return [];
}
if (! $this->active($user->tenant_id)) {
return $user->purchasing_role === null || $user->purchasing_role === PurchasingRole::Configured ? [] : [$user->purchasing_role->value];
}
$contextKey = $context;
if ($contextKey === null && app()->bound('request') && request()->hasSession() && WorkContext::area(request()->path()) !== null && app(WorkContext::class)->enforced($user)) {
$contextKey = app(WorkContext::class)->active(request());
if ($contextKey === null) {
return [];
}
}
$columns = ['p.permissions'];
if (Schema::hasColumn('organization_profiles', 'work_contexts')) {
$columns[] = 'p.work_contexts';
}
$rows = DB::table('organization_memberships as m')->join('organization_profiles as p', 'p.id', '=', 'm.profile_id')
->where('m.tenant_id', $user->tenant_id)->where('p.tenant_id', $user->tenant_id)->where('m.user_id', $user->id)
->where(fn ($q) => $q->whereNull('m.expires_at')->orWhere('m.expires_at', '>', now()))->get($columns);
$permissions = [];
foreach ($rows as $row) {
if ($contextKey !== null && ! in_array($contextKey, json_decode($row->work_contexts ?? '[]', true, 32, JSON_THROW_ON_ERROR), true)) {
continue;
}
$permissions = [...$permissions, ...json_decode($row->permissions, true, 32, JSON_THROW_ON_ERROR)];
}
return array_values(array_unique($permissions));
}
/** @param list<PurchasingRole|string> $functions */
public function allows(User $user, array $functions): bool
{
$requested = array_map(fn ($f) => $f instanceof PurchasingRole ? $f->value : $f, $functions);
return $user->tenant_id !== null && count(array_intersect($this->permissions($user), $requested)) > 0;
}
public function maintenanceOnly(User $user): bool
{
if (! $this->active($user->tenant_id)) {
return false;
}
$valid = DB::table('organization_memberships')->where('tenant_id', $user->tenant_id)->where('user_id', $user->id)->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()));
return (clone $valid)->where('maintenance', true)->exists() && ! (clone $valid)->where('maintenance', false)->exists();
}
public function departmentScoped(User $user): bool
{
$permissions = $this->permissions($user);
return in_array('department_head', $permissions, true) && ! array_intersect($permissions, ['admin', 'procurement', 'finance']);
}
public function canRequest(User $user): bool
{
return $user->tenant_id !== null && ($this->active($user->tenant_id) ? $this->allows($user, ['requester']) : ($user->purchasing_role !== null && $user->purchasing_role !== PurchasingRole::Configured));
}
public function profileLabel(User $user): string
{
if (! $this->active($user->tenant_id)) {
return $user->purchasing_role?->label() ?? 'Utilizador';
}
return DB::table('organization_memberships as m')->join('organization_profiles as p', 'p.id', '=', 'm.profile_id')->where('m.tenant_id', $user->tenant_id)->where('p.tenant_id', $user->tenant_id)->where('m.user_id', $user->id)
->where(fn ($q) => $q->whereNull('m.expires_at')->orWhere('m.expires_at', '>', now()))->orderBy('p.name')->pluck('p.name')->unique()->implode(' · ');
}
/** @param list<string> $fields
* @return array<string,mixed>
*/
public function actorSnapshot(User $user, array $fields): array
{
$snapshot = $user->only($fields);
if ($this->active($user->tenant_id)) {
if (array_key_exists('purchasing_role', $snapshot)) {
$snapshot['purchasing_role'] = $user->purchasing_role->value ?? '';
}
$snapshot['profile_label'] = $this->profileLabel($user);
}
return $snapshot;
}
}